Initial commit
This commit is contained in:
commit
08a8a71c55
631 changed files with 139902 additions and 0 deletions
248
public/kirby/src/Content/Content.php
Normal file
248
public/kirby/src/Content/Content.php
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Kirby\Cms\Blueprint;
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Form\Form;
|
||||
|
||||
/**
|
||||
* The Content class handles all fields
|
||||
* for content from pages, the site and users
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Content
|
||||
{
|
||||
/**
|
||||
* The raw data array
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Cached field objects
|
||||
* Once a field is being fetched
|
||||
* it is added to this array for
|
||||
* later reuse
|
||||
*/
|
||||
protected array $fields = [];
|
||||
|
||||
/**
|
||||
* A potential parent object.
|
||||
* Not necessarily needed. Especially
|
||||
* for testing, but field methods might
|
||||
* need it.
|
||||
*/
|
||||
protected ModelWithContent|null $parent;
|
||||
|
||||
/**
|
||||
* Magic getter for content fields
|
||||
*/
|
||||
public function __call(string $name, array $arguments = []): Field
|
||||
{
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Content object
|
||||
*
|
||||
* @param bool $normalize Set to `false` if the input field keys are already lowercase
|
||||
*/
|
||||
public function __construct(
|
||||
array $data = [],
|
||||
ModelWithContent $parent = null,
|
||||
bool $normalize = true
|
||||
) {
|
||||
if ($normalize === true) {
|
||||
$data = array_change_key_case($data, CASE_LOWER);
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `self::data()` to improve
|
||||
* `var_dump` output
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @see self::data()
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the content to a new blueprint
|
||||
*/
|
||||
public function convertTo(string $to): array
|
||||
{
|
||||
// prepare data
|
||||
$data = [];
|
||||
$content = $this;
|
||||
|
||||
// blueprints
|
||||
$old = $this->parent->blueprint();
|
||||
$subfolder = dirname($old->name());
|
||||
$new = Blueprint::factory(
|
||||
$subfolder . '/' . $to,
|
||||
$subfolder . '/default',
|
||||
$this->parent
|
||||
);
|
||||
|
||||
// forms
|
||||
$oldForm = new Form([
|
||||
'fields' => $old->fields(),
|
||||
'model' => $this->parent
|
||||
]);
|
||||
$newForm = new Form([
|
||||
'fields' => $new->fields(),
|
||||
'model' => $this->parent
|
||||
]);
|
||||
|
||||
// fields
|
||||
$oldFields = $oldForm->fields();
|
||||
$newFields = $newForm->fields();
|
||||
|
||||
// go through all fields of new template
|
||||
foreach ($newFields as $newField) {
|
||||
$name = $newField->name();
|
||||
$oldField = $oldFields->get($name);
|
||||
|
||||
// field name and type matches with old template
|
||||
if ($oldField?->type() === $newField->type()) {
|
||||
$data[$name] = $content->get($name)->value();
|
||||
} else {
|
||||
$data[$name] = $newField->default();
|
||||
}
|
||||
}
|
||||
|
||||
// preserve existing fields
|
||||
return array_merge($this->data, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data array
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered field objects
|
||||
*/
|
||||
public function fields(): array
|
||||
{
|
||||
foreach ($this->data as $key => $value) {
|
||||
$this->get($key);
|
||||
}
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either a single field object
|
||||
* or all registered fields
|
||||
*/
|
||||
public function get(string $key = null): Field|array
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->fields();
|
||||
}
|
||||
|
||||
$key = strtolower($key);
|
||||
|
||||
return $this->fields[$key] ??= new Field(
|
||||
$this->parent,
|
||||
$key,
|
||||
$this->data()[$key] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a content field is set
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->data[strtolower($key)]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all field keys
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return array_keys($this->data());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of the content object
|
||||
* without the fields, specified by the
|
||||
* passed key(s)
|
||||
*/
|
||||
public function not(string ...$keys): static
|
||||
{
|
||||
$copy = clone $this;
|
||||
$copy->fields = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
unset($copy->data[strtolower($key)]);
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent
|
||||
* Site, Page, File or User object
|
||||
*/
|
||||
public function parent(): ModelWithContent|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parent model
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setParent(ModelWithContent $parent): static
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data array
|
||||
*
|
||||
* @see self::data()
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content and returns
|
||||
* a cloned object
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function update(
|
||||
array $content = null,
|
||||
bool $overwrite = false
|
||||
): static {
|
||||
$content = array_change_key_case((array)$content, CASE_LOWER);
|
||||
$this->data = $overwrite === true ? $content : array_merge($this->data, $content);
|
||||
|
||||
// clear cache of Field objects
|
||||
$this->fields = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
314
public/kirby/src/Content/ContentStorage.php
Normal file
314
public/kirby/src/Content/ContentStorage.php
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Generator;
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Wrapper for the ContentStorageHandler to
|
||||
* bundle some business logic that should not
|
||||
* be included in the handlers themselves but
|
||||
* also not in the general code calling the storage
|
||||
* methods
|
||||
*
|
||||
* @internal
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class ContentStorage
|
||||
{
|
||||
protected ContentStorageHandler $handler;
|
||||
|
||||
public function __construct(
|
||||
protected ModelWithContent $model,
|
||||
string $handler = PlainTextContentStorageHandler::class
|
||||
) {
|
||||
$this->handler = new $handler($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic caller for handler methods
|
||||
*/
|
||||
public function __call(string $name, array $args): mixed
|
||||
{
|
||||
return $this->handler->$name(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns generator for all existing versions-languages combinations
|
||||
*
|
||||
* @return Generator<string|string>
|
||||
* @todo 4.0.0 consider more descpritive name
|
||||
*/
|
||||
public function all(): Generator
|
||||
{
|
||||
foreach ($this->model->kirby()->languages()->codes() as $lang) {
|
||||
foreach ($this->dynamicVersions() as $version) {
|
||||
if ($this->exists($version, $lang) === true) {
|
||||
yield $version => $lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file
|
||||
* @internal eventually should only exists in PlainTextContentStorage,
|
||||
* when not relying anymore on language helper
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*
|
||||
* @throws \Kirby\Exception\LogicException If the model type doesn't have a known content filename
|
||||
*/
|
||||
public function contentFile(
|
||||
string $version,
|
||||
string $lang,
|
||||
bool $force = false
|
||||
): string {
|
||||
$lang = $this->language($lang, $force);
|
||||
return $this->handler->contentFile($version, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts all versions when converting languages
|
||||
* @internal
|
||||
*/
|
||||
public function convertLanguage(string $from, string $to): void
|
||||
{
|
||||
$from = $this->language($from, true);
|
||||
$to = $this->language($to, true);
|
||||
|
||||
foreach ($this->dynamicVersions() as $version) {
|
||||
$this->handler->move($version, $from, $version, $to);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new version
|
||||
*
|
||||
* @param string|null $lang Code `'default'` in a single-lang installation
|
||||
* @param array<string, string> $fields Content fields
|
||||
*/
|
||||
public function create(
|
||||
string $versionType,
|
||||
string|null $lang,
|
||||
array $fields
|
||||
): void {
|
||||
$lang = $this->language($lang);
|
||||
$this->handler->create($versionType, $lang, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default version identifier for the model
|
||||
* @internal
|
||||
*/
|
||||
public function defaultVersion(): string
|
||||
{
|
||||
if (
|
||||
$this->model instanceof Page === true &&
|
||||
$this->model->isDraft() === true
|
||||
) {
|
||||
return 'changes';
|
||||
}
|
||||
|
||||
return 'published';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing version in an idempotent way if it was already deleted
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function delete(
|
||||
string $version,
|
||||
string|null $lang = null,
|
||||
bool $force = false
|
||||
): void {
|
||||
$lang = $this->language($lang, $force);
|
||||
$this->handler->delete($version, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versions when deleting a language
|
||||
* @internal
|
||||
*/
|
||||
public function deleteLanguage(string|null $lang): void
|
||||
{
|
||||
$lang = $this->language($lang, true);
|
||||
|
||||
foreach ($this->dynamicVersions() as $version) {
|
||||
$this->handler->delete($version, $lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all versions availalbe for the model that can be updated
|
||||
* @internal
|
||||
*/
|
||||
public function dynamicVersions(): array
|
||||
{
|
||||
$versions = ['changes'];
|
||||
|
||||
if (
|
||||
$this->model instanceof Page === false ||
|
||||
$this->model->isDraft() === false
|
||||
) {
|
||||
$versions[] = 'published';
|
||||
}
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version exists
|
||||
*
|
||||
* @param string|null $lang Code `'default'` in a single-lang installation;
|
||||
* checks for "any language" if not provided
|
||||
*/
|
||||
public function exists(
|
||||
string $version,
|
||||
string|null $lang
|
||||
): bool {
|
||||
if ($lang !== null) {
|
||||
$lang = $this->language($lang);
|
||||
}
|
||||
|
||||
return $this->handler->exists($version, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modification timestamp of a version
|
||||
* if it exists
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function modified(
|
||||
string $version,
|
||||
string|null $lang = null
|
||||
): int|null {
|
||||
$lang = $this->language($lang);
|
||||
return $this->handler->modified($version, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored content fields
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @return array<string, string>
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function read(
|
||||
string $version,
|
||||
string|null $lang = null
|
||||
): array {
|
||||
$lang = $this->language($lang);
|
||||
$this->ensureExistingVersion($version, $lang);
|
||||
return $this->handler->read($version, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the modification timestamp of an existing version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function touch(
|
||||
string $version,
|
||||
string|null $lang = null
|
||||
): void {
|
||||
$lang = $this->language($lang);
|
||||
$this->ensureExistingVersion($version, $lang);
|
||||
$this->handler->touch($version, $lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Touches all versions of a language
|
||||
* @internal
|
||||
*/
|
||||
public function touchLanguage(string|null $lang): void
|
||||
{
|
||||
$lang = $this->language($lang, true);
|
||||
|
||||
foreach ($this->dynamicVersions() as $version) {
|
||||
if ($this->exists($version, $lang) === true) {
|
||||
$this->handler->touch($version, $lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content fields of an existing version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @param array<string, string> $fields Content fields
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function update(
|
||||
string $version,
|
||||
string|null $lang = null,
|
||||
array $fields = []
|
||||
): void {
|
||||
$lang = $this->language($lang);
|
||||
$this->ensureExistingVersion($version, $lang);
|
||||
$this->handler->update($version, $lang, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
protected function ensureExistingVersion(
|
||||
string $version,
|
||||
string $lang
|
||||
): void {
|
||||
if ($this->exists($version, $lang) !== true) {
|
||||
throw new NotFoundException('Version "' . $version . ' (' . $lang . ')" does not already exist');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a "user-facing" language code to a "raw" language code to be
|
||||
* used for storage
|
||||
*
|
||||
* @param bool $force If set to `true`, the language code is not validated
|
||||
* @return string Language code
|
||||
*/
|
||||
protected function language(
|
||||
string|null $languageCode = null,
|
||||
bool $force = false
|
||||
): string {
|
||||
// in force mode, use the provided language code even in single-lang for
|
||||
// compatibility with the previous behavior in `$model->contentFile()`
|
||||
if ($force === true) {
|
||||
return $languageCode ?? 'default';
|
||||
}
|
||||
|
||||
// in multi-lang, …
|
||||
if ($this->model->kirby()->multilang() === true) {
|
||||
// look up the actual language object if possible
|
||||
$language = $this->model->kirby()->language($languageCode);
|
||||
|
||||
// validate the language code
|
||||
if ($language === null) {
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
|
||||
return $language->code();
|
||||
}
|
||||
|
||||
// otherwise use hardcoded "default" code for single lang
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
96
public/kirby/src/Content/ContentStorageHandler.php
Normal file
96
public/kirby/src/Content/ContentStorageHandler.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* Interface for content storage handlers;
|
||||
* note that it is so far not viable to build custom
|
||||
* handlers because the CMS core relies on the filesystem
|
||||
* and cannot fully benefit from this abstraction yet
|
||||
* @internal
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
interface ContentStorageHandler
|
||||
{
|
||||
public function __construct(ModelWithContent $model);
|
||||
|
||||
/**
|
||||
* Creates a new version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @param array<string, string> $fields Content fields
|
||||
*/
|
||||
public function create(string $versionType, string $lang, array $fields): void;
|
||||
|
||||
/**
|
||||
* Deletes an existing version in an idempotent way if it was already deleted
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function delete(string $version, string $lang): void;
|
||||
|
||||
/**
|
||||
* Checks if a version exists
|
||||
*
|
||||
* @param string|null $lang Code `'default'` in a single-lang installation;
|
||||
* checks for "any language" if not provided
|
||||
*/
|
||||
public function exists(string $version, string|null $lang): bool;
|
||||
|
||||
/**
|
||||
* Returns the modification timestamp of a version if it exists
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function modified(string $version, string $lang): int|null;
|
||||
|
||||
/**
|
||||
* Moves content from one version-language combination to another
|
||||
*
|
||||
* @param string $fromLang Code `'default'` in a single-lang installation
|
||||
* @param string $toLang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function move(
|
||||
string $fromVersion,
|
||||
string $fromLang,
|
||||
string $toVersion,
|
||||
string $toLang
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Returns the stored content fields
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @return array<string, string>
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function read(string $version, string $lang): array;
|
||||
|
||||
/**
|
||||
* Updates the modification timestamp of an existing version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function touch(string $version, string $lang): void;
|
||||
|
||||
/**
|
||||
* Updates the content fields of an existing version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @param array<string, string> $fields Content fields
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function update(string $version, string $lang, array $fields): void;
|
||||
}
|
||||
173
public/kirby/src/Content/ContentTranslation.php
Normal file
173
public/kirby/src/Content/ContentTranslation.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* Each page, file or site can have multiple
|
||||
* translated versions of their content,
|
||||
* represented by this class
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class ContentTranslation
|
||||
{
|
||||
protected string $code;
|
||||
protected array|null $content;
|
||||
protected string $contentFile;
|
||||
protected ModelWithContent $parent;
|
||||
protected string|null $slug;
|
||||
|
||||
/**
|
||||
* Creates a new translation object
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->code = $props['code'];
|
||||
$this->parent = $props['parent'];
|
||||
$this->slug = $props['slug'] ?? null;
|
||||
|
||||
if ($content = $props['content'] ?? null) {
|
||||
$this->content = array_change_key_case($content);
|
||||
} else {
|
||||
$this->content = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve `var_dump` output
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language code of the
|
||||
* translation
|
||||
*/
|
||||
public function code(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation content
|
||||
* as plain array
|
||||
*/
|
||||
public function content(): array
|
||||
{
|
||||
$parent = $this->parent();
|
||||
$content = $this->content ??= $parent->readContent($this->code());
|
||||
|
||||
// merge with the default content
|
||||
if (
|
||||
$this->isDefault() === false &&
|
||||
$defaultLanguage = $parent->kirby()->defaultLanguage()
|
||||
) {
|
||||
$content = array_merge(
|
||||
$parent->translation($defaultLanguage->code())?->content() ?? [],
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to the translation content file
|
||||
*/
|
||||
public function contentFile(): string
|
||||
{
|
||||
// temporary compatibility change (TODO: take this from the parent `ModelVersion` object)
|
||||
$identifier = $this->parent::CLASS_ALIAS === 'page' && $this->parent->isDraft() === true ?
|
||||
'changes' :
|
||||
'published';
|
||||
|
||||
return $this->contentFile = $this->parent->storage()->contentFile(
|
||||
$identifier,
|
||||
$this->code,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the translation file exists
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return
|
||||
empty($this->content) === false ||
|
||||
file_exists($this->contentFile()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation code as id
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the this is the default translation
|
||||
* of the model
|
||||
*/
|
||||
public function isDefault(): bool
|
||||
{
|
||||
return $this->code() === $this->parent->kirby()->defaultLanguage()?->code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent page, file or site object
|
||||
*/
|
||||
public function parent(): ModelWithContent
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom translation slug
|
||||
*/
|
||||
public function slug(): string|null
|
||||
{
|
||||
return $this->slug ??= ($this->content()['slug'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the old and new data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function update(array $data = null, bool $overwrite = false): static
|
||||
{
|
||||
$data = array_change_key_case((array)$data);
|
||||
|
||||
$this->content = match ($overwrite) {
|
||||
true => $data,
|
||||
default => array_merge($this->content(), $data)
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the most important translation
|
||||
* props to an array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->code(),
|
||||
'content' => $this->content(),
|
||||
'exists' => $this->exists(),
|
||||
'slug' => $this->slug(),
|
||||
];
|
||||
}
|
||||
}
|
||||
220
public/kirby/src/Content/Field.php
Normal file
220
public/kirby/src/Content/Field.php
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* Every field in a Kirby content text file
|
||||
* is being converted into such a Field object.
|
||||
*
|
||||
* Field methods can be registered for those Field
|
||||
* objects, which can then be used to transform or
|
||||
* convert the field value. This enables our
|
||||
* daisy-chaining API for templates and other components
|
||||
*
|
||||
* ```php
|
||||
* // Page field example with lowercase conversion
|
||||
* $page->myField()->lower();
|
||||
* ```
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Field
|
||||
{
|
||||
/**
|
||||
* Field method aliases
|
||||
*/
|
||||
public static array $aliases = [];
|
||||
|
||||
/**
|
||||
* The field name
|
||||
*/
|
||||
protected string $key;
|
||||
|
||||
/**
|
||||
* Registered field methods
|
||||
*/
|
||||
public static array $methods = [];
|
||||
|
||||
/**
|
||||
* The parent object if available.
|
||||
* This will be the page, site, user or file
|
||||
* to which the content belongs
|
||||
*/
|
||||
protected ModelWithContent|null $parent;
|
||||
|
||||
/**
|
||||
* The value of the field
|
||||
*/
|
||||
public mixed $value;
|
||||
|
||||
/**
|
||||
* Creates a new field object
|
||||
*/
|
||||
public function __construct(
|
||||
ModelWithContent|null $parent,
|
||||
string $key,
|
||||
mixed $value
|
||||
) {
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic caller for field methods
|
||||
*/
|
||||
public function __call(string $method, array $arguments = []): mixed
|
||||
{
|
||||
$method = strtolower($method);
|
||||
|
||||
if (isset(static::$methods[$method]) === true) {
|
||||
return (static::$methods[$method])(clone $this, ...$arguments);
|
||||
}
|
||||
|
||||
if (isset(static::$aliases[$method]) === true) {
|
||||
$method = strtolower(static::$aliases[$method]);
|
||||
|
||||
if (isset(static::$methods[$method]) === true) {
|
||||
return (static::$methods[$method])(clone $this, ...$arguments);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies the var_dump result
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @see Field::toArray
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes it possible to simply echo
|
||||
* or stringify the entire object
|
||||
*
|
||||
* @see Field::toString
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field exists in the content data array
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->parent->content()->has($this->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field content is empty
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return
|
||||
empty($this->value) === true &&
|
||||
in_array($this->value, [0, '0', false], true) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field content is not empty
|
||||
*/
|
||||
public function isNotEmpty(): bool
|
||||
{
|
||||
return $this->isEmpty() === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the field
|
||||
*/
|
||||
public function key(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Field::parent()
|
||||
*/
|
||||
public function model(): ModelWithContent|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a fallback if the field value is empty
|
||||
*
|
||||
* @return $this|static
|
||||
*/
|
||||
public function or(mixed $fallback = null): static
|
||||
{
|
||||
if ($this->isNotEmpty()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($fallback instanceof self) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$field = clone $this;
|
||||
$field->value = $fallback;
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent object of the field
|
||||
*/
|
||||
public function parent(): ModelWithContent|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Field object to an array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [$this->key => $this->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field value as string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return (string)$this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field content. If a new value is passed,
|
||||
* the modified field will be returned. Otherwise it
|
||||
* will return the field value.
|
||||
*/
|
||||
public function value(string|Closure $value = null): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
if ($value instanceof Closure) {
|
||||
$value = $value->call($this, $this->value);
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->value = (string)$value;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
253
public/kirby/src/Content/PlainTextContentStorageHandler.php
Normal file
253
public/kirby/src/Content/PlainTextContentStorageHandler.php
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
|
||||
/**
|
||||
* Content storage handler using plain text files
|
||||
* stored in the content folder
|
||||
* @internal
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PlainTextContentStorageHandler implements ContentStorageHandler
|
||||
{
|
||||
public function __construct(protected ModelWithContent $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @param array<string, string> $fields Content fields
|
||||
*/
|
||||
public function create(string $versionType, string $lang, array $fields): void
|
||||
{
|
||||
$success = Data::write($this->contentFile($versionType, $lang), $fields);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception('Could not write new content file');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing version in an idempotent way if it was already deleted
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function delete(string $version, string $lang): void
|
||||
{
|
||||
$contentFile = $this->contentFile($version, $lang);
|
||||
$success = F::unlink($contentFile);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception('Could not delete content file');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
// clean up empty directories
|
||||
$contentDir = dirname($contentFile);
|
||||
if (
|
||||
Dir::exists($contentDir) === true &&
|
||||
Dir::isEmpty($contentDir) === true
|
||||
) {
|
||||
$success = rmdir($contentDir);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception('Could not delete empty content directory');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version exists
|
||||
*
|
||||
* @param string|null $lang Code `'default'` in a single-lang installation;
|
||||
* checks for "any language" if not provided
|
||||
*/
|
||||
public function exists(string $version, string|null $lang): bool
|
||||
{
|
||||
if ($lang === null) {
|
||||
foreach ($this->contentFiles($version) as $file) {
|
||||
if (is_file($file) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_file($this->contentFile($version, $lang)) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modification timestamp of a version
|
||||
* if it exists
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function modified(string $version, string $lang): int|null
|
||||
{
|
||||
$modified = F::modified($this->contentFile($version, $lang));
|
||||
|
||||
if (is_int($modified) === true) {
|
||||
return $modified;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored content fields
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @return array<string, string>
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function read(string $version, string $lang): array
|
||||
{
|
||||
return Data::read($this->contentFile($version, $lang));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the modification timestamp of an existing version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function touch(string $version, string $lang): void
|
||||
{
|
||||
$success = touch($this->contentFile($version, $lang));
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception('Could not touch existing content file');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content fields of an existing version
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
* @param array<string, string> $fields Content fields
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the version does not exist
|
||||
*/
|
||||
public function update(string $version, string $lang, array $fields): void
|
||||
{
|
||||
$success = Data::write($this->contentFile($version, $lang), $fields);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception('Could not write existing content file');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file
|
||||
* @internal To be made `protected` when the CMS core no longer relies on it
|
||||
*
|
||||
* @param string $lang Code `'default'` in a single-lang installation
|
||||
*
|
||||
* @throws \Kirby\Exception\LogicException If the model type doesn't have a known content filename
|
||||
*/
|
||||
public function contentFile(string $version, string $lang): string
|
||||
{
|
||||
if (in_array($version, ['published', 'changes']) !== true) {
|
||||
throw new InvalidArgumentException('Invalid version identifier "' . $version . '"');
|
||||
}
|
||||
|
||||
$extension = $this->model->kirby()->contentExtension();
|
||||
$directory = $this->model->root();
|
||||
|
||||
$directory = match ($this->model::CLASS_ALIAS) {
|
||||
'file' => dirname($this->model->root()),
|
||||
default => $this->model->root()
|
||||
};
|
||||
|
||||
$filename = match ($this->model::CLASS_ALIAS) {
|
||||
'file' => $this->model->filename(),
|
||||
'page' => $this->model->intendedTemplate()->name(),
|
||||
'site',
|
||||
'user' => $this->model::CLASS_ALIAS,
|
||||
// @codeCoverageIgnoreStart
|
||||
default => throw new LogicException('Cannot determine content filename for model type "' . $this->model::CLASS_ALIAS . '"')
|
||||
// @codeCoverageIgnoreEnd
|
||||
};
|
||||
|
||||
if ($this->model::CLASS_ALIAS === 'page' && $this->model->isDraft() === true) {
|
||||
// changes versions don't need anything extra
|
||||
// (drafts already have the `_drafts` prefix in their root),
|
||||
// but a published version is not possible
|
||||
if ($version === 'published') {
|
||||
throw new LogicException('Drafts cannot have a published content file');
|
||||
}
|
||||
} elseif ($version === 'changes') {
|
||||
// other model type or published page that has a changes subfolder
|
||||
$directory .= '/_changes';
|
||||
}
|
||||
|
||||
if ($lang !== 'default') {
|
||||
return $directory . '/' . $filename . '.' . $lang . '.' . $extension;
|
||||
}
|
||||
|
||||
return $directory . '/' . $filename . '.' . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with content files of all languages
|
||||
* @internal To be made `protected` when the CMS core no longer relies on it
|
||||
*/
|
||||
public function contentFiles(string $version): array
|
||||
{
|
||||
if ($this->model->kirby()->multilang() === true) {
|
||||
return $this->model->kirby()->languages()->values(
|
||||
fn ($lang) => $this->contentFile($version, $lang)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
$this->contentFile($version, 'default')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves content from one version-language combination to another
|
||||
*
|
||||
* @param string $fromLang Code `'default'` in a single-lang installation
|
||||
* @param string $toLang Code `'default'` in a single-lang installation
|
||||
*/
|
||||
public function move(
|
||||
string $fromVersion,
|
||||
string $fromLang,
|
||||
string $toVersion,
|
||||
string $toLang
|
||||
): void {
|
||||
F::move(
|
||||
$this->contentFile($fromVersion, $fromLang),
|
||||
$this->contentFile($toVersion, $toLang)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue