julienmonnerie/kirby/src/Content/PlainTextContentStorageHandler.php
2025-04-21 18:57:21 +02:00

253 lines
6.9 KiB
PHP

<?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)
);
}
}