@@ -487,7 +470,7 @@ protected function addSlice($sliceId, $moduleId)
$fragment = new Fragment();
$fragment->setVar('before', $msg, false);
$fragment->setVar('class', 'add', false);
- $fragment->setVar('title', I18n::msg('module') . ': ' . I18n::translate((string) $MOD->getValue('name')), false);
+ $fragment->setVar('title', I18n::msg('module') . ': ' . I18n::translate($module->name), false);
$fragment->setVar('body', $panel, false);
$fragment->setVar('footer', $sliceFooter, false);
$sliceContent = $fragment->parse('core/page/section.php');
@@ -499,16 +482,7 @@ protected function addSlice($sliceId, $moduleId)
return $fragment->parse('core/structure/content/slice_list_item.php');
}
- // ----- EDIT Slice
- /**
- * @param int $sliceId
- * @param string $moduleInput
- * @param int $ctypeId
- * @param int $moduleId
- * @param Sql $artDataSql
- * @return string
- */
- protected function editSlice($sliceId, $moduleInput, $ctypeId, $moduleId, $artDataSql)
+ protected function editSlice(int $sliceId, ArticleSlice $slice, int $ctypeId, string $moduleKey, Sql $artDataSql): string
{
$msg = '';
if ($this->slice_id == $sliceId) {
@@ -538,21 +512,24 @@ protected function editSlice($sliceId, $moduleInput, $ctypeId, $moduleId, $artDa
$fragment->setVar('elements', $formElements, false);
$sliceFooter = $fragment->parse('core/form/submit.php');
+ $module = Module::get($moduleKey);
+ $moduleInput = $module ? $module->input($slice) : '';
+
$panel = '
';
$fragment = new Fragment();
$fragment->setVar('class', 'edit', false);
- $fragment->setVar('title', $this->getSliceHeading($artDataSql), false);
+ $fragment->setVar('title', $this->getSliceHeading($moduleKey), false);
$fragment->setVar('options', $this->getSliceMenu($artDataSql), false);
$fragment->setVar('body', $panel, false);
$fragment->setVar('footer', $sliceFooter, false);
diff --git a/src/Content/ArticleHandler.php b/src/Content/ArticleHandler.php
index f8308b8936..41f25b7fb0 100644
--- a/src/Content/ArticleHandler.php
+++ b/src/Content/ArticleHandler.php
@@ -51,10 +51,10 @@ public static function addArticle(array $data): string
// Wenn Template nicht vorhanden, dann entweder erlaubtes nehmen
// oder leer setzen.
- if (!isset($templates[$data['template_id']])) {
- $data['template_id'] = 0;
+ if (!isset($templates[$data['template']])) {
+ $data['template'] = null;
if (count($templates) > 0) {
- $data['template_id'] = key($templates);
+ $data['template'] = key($templates);
}
}
@@ -86,7 +86,7 @@ public static function addArticle(array $data): string
$AART->setValue('path', $path);
$AART->setValue('startarticle', 0);
$AART->setValue('status', 0);
- $AART->setValue('template_id', $data['template_id']);
+ $AART->setValue('template', $data['template']);
$AART->addGlobalCreateFields($user);
$AART->addGlobalUpdateFields($user);
@@ -105,7 +105,7 @@ public static function addArticle(array $data): string
'parent_id' => $data['category_id'],
'priority' => $data['priority'],
'path' => $path,
- 'template_id' => $data['template_id'],
+ 'template_key' => $data['template'],
'data' => $data,
]));
}
@@ -143,10 +143,10 @@ public static function editArticle(int $articleId, int $clang, array $data): str
// Wenn Template nicht vorhanden, dann entweder erlaubtes nehmen
// oder leer setzen.
- if (!isset($templates[$data['template_id']])) {
- $data['template_id'] = 0;
+ if (!isset($templates[$data['template']])) {
+ $data['template'] = null;
if (count($templates) > 0) {
- $data['template_id'] = key($templates);
+ $data['template'] = key($templates);
}
}
@@ -164,7 +164,7 @@ public static function editArticle(int $articleId, int $clang, array $data): str
$EA->setTable(Core::getTablePrefix() . 'article');
$EA->setWhere(['id' => $articleId, 'clang_id' => $clang]);
$EA->setValue('name', $data['name']);
- $EA->setValue('template_id', $data['template_id']);
+ $EA->setValue('template', $data['template']);
$EA->setValue('priority', $data['priority']);
$EA->addGlobalUpdateFields(self::getUser());
@@ -200,7 +200,7 @@ public static function editArticle(int $articleId, int $clang, array $data): str
'parent_id' => $data['category_id'],
'priority' => $data['priority'],
'path' => $data['path'],
- 'template_id' => $data['template_id'],
+ 'template_key' => $data['template'],
'data' => $data,
]));
@@ -238,7 +238,7 @@ public static function deleteArticle($articleId)
'status' => $Art->getValue('status'),
'priority' => $Art->getValue('priority'),
'path' => $Art->getValue('path'),
- 'template_id' => $Art->getValue('template_id'),
+ 'template_key' => $Art->getValue('template'),
]));
$Art->next();
@@ -294,7 +294,7 @@ public static function _deleteArticle($id)
'status' => $ART->getValue('status'),
'priority' => $ART->getValue('priority'),
'path' => $ART->getValue('path'),
- 'template_id' => $ART->getValue('template_id'),
+ 'template_key' => $ART->getValue('template'),
]));
if (1 == $ART->getValue('startarticle')) {
diff --git a/src/Content/ArticleSlice.php b/src/Content/ArticleSlice.php
index 9f084cde33..b7e315f76a 100644
--- a/src/Content/ArticleSlice.php
+++ b/src/Content/ArticleSlice.php
@@ -28,7 +28,7 @@ private function __construct(
public int $articleId,
public int $clangId,
public int $contentSectionId,
- public int $moduleId,
+ public string $moduleKey,
public int $priority,
public int $status,
public int $createdate,
@@ -48,7 +48,7 @@ public static function forNewSlice(
int $articleId,
int $clangId,
int $ctype,
- int $moduleId,
+ string $moduleKey,
int $priority,
int $revision,
): self {
@@ -57,7 +57,7 @@ public static function forNewSlice(
$articleId,
$clangId,
$ctype,
- $moduleId,
+ $moduleKey,
$priority,
1,
$time = time(),
@@ -91,7 +91,7 @@ public static function fromSql(Sql $sql): self
(int) $sql->getValue($table . '.article_id'),
(int) $sql->getValue($table . '.clang_id'),
(int) $sql->getValue($table . '.ctype_id'),
- (int) $sql->getValue($table . '.module_id'),
+ (string) $sql->getValue($table . '.module'),
(int) $sql->getValue($table . '.priority'),
(int) $sql->getValue($table . '.status'),
(int) $sql->getDateTimeValue($table . '.createdate'),
@@ -167,13 +167,13 @@ public static function getSlicesForArticle(int $articleId, ?int $clang = null, i
*
* @return list
*/
- public static function getSlicesForArticleOfType(int $articleId, int $moduleId, ?int $clang = null, int $revision = 0, bool $ignoreOfflines = false): array
+ public static function getSlicesForArticleOfType(int $articleId, string $moduleKey, ?int $clang = null, int $revision = 0, bool $ignoreOfflines = false): array
{
$clang ??= Language::getCurrentId();
return self::getSlicesWhere(
- 'article_id=? AND clang_id=? AND module_id=? AND revision=?' . ($ignoreOfflines ? ' AND status = 1' : ''),
- [$articleId, $clang, $moduleId, $revision],
+ 'article_id=? AND clang_id=? AND module=? AND revision=?' . ($ignoreOfflines ? ' AND status = 1' : ''),
+ [$articleId, $clang, $moduleKey, $revision],
);
}
@@ -373,16 +373,16 @@ public function isOnline(): bool
/**
* @internal
- * @param array{id: int, articleId: int, clang: int, ctype: int, moduleId: int, priority: int, status: int, createdate: int, updatedate: int, createuser: string, updateuser: string, revision: int, values: array, media: array, medialists: array, links: array, linklists: array} $data
+ * @param array{id: int, articleId: int, clangId: int, contentSectionId: int, moduleKey: string, priority: int, status: int, createdate: int, updatedate: int, createuser: string, updateuser: string, revision: int, values: array, media: array, medialists: array, links: array, linklists: array} $data
*/
public static function __set_state(array $data): self
{
return new self(
$data['id'],
$data['articleId'],
- $data['clang'],
- $data['ctype'],
- $data['moduleId'],
+ $data['clangId'],
+ $data['contentSectionId'],
+ $data['moduleKey'],
$data['priority'],
$data['status'],
$data['createdate'],
diff --git a/src/Content/ArticleSliceAction.php b/src/Content/ArticleSliceAction.php
index 3f515da6ec..c3f3899709 100644
--- a/src/Content/ArticleSliceAction.php
+++ b/src/Content/ArticleSliceAction.php
@@ -2,57 +2,34 @@
namespace Redaxo\Core\Content;
-use Redaxo\Core\Core;
use Redaxo\Core\Database\Sql;
-use Redaxo\Core\Exception\InvalidArgumentException;
use Redaxo\Core\Http\Request;
-use Redaxo\Core\Util\Stream;
-use function in_array;
use function is_array;
final class ArticleSliceAction
{
- public const int ADD = 1;
- public const int EDIT = 2;
- public const int DELETE = 4;
-
- public const string PREVIEW = 'preview';
- public const string PRESAVE = 'presave';
- public const string POSTSAVE = 'postsave';
-
- public readonly int $articleId;
- public readonly int $clangId;
- public readonly int $ctypeId;
- public readonly int $sliceId;
-
- /** @param self::ADD|self::EDIT|self::DELETE $mode */
- public readonly int $mode;
+ public const string ADD = 'add';
+ public const string EDIT = 'edit';
+ public const string DELETE = 'delete';
public bool $save = true;
/** @var list */
public private(set) array $messages = [];
- /** @internal */
+ /**
+ * @param self::ADD|self::EDIT|self::DELETE $mode
+ * @internal
+ */
public function __construct(
- public readonly int $moduleId,
- public readonly string $event,
+ public readonly string $mode,
+ public readonly int $articleId,
+ public readonly int $clangId,
+ public readonly int $ctypeId,
+ public readonly int $sliceId,
private readonly Sql $sql,
- ) {
- if ('edit' == $event) {
- $this->mode = self::EDIT;
- } elseif ('delete' == $event) {
- $this->mode = self::DELETE;
- } else {
- $this->mode = self::ADD;
- }
-
- $this->articleId = Request::request('article_id', 'int');
- $this->clangId = Request::request('clang', 'int');
- $this->ctypeId = Request::request('ctype', 'int');
- $this->sliceId = self::ADD === $this->mode ? 0 : Request::request('slice_id', 'int');
- }
+ ) {}
/** @internal */
public function setRequestValues(): void
@@ -74,57 +51,37 @@ public function setRequestValues(): void
}
}
- /** @param self::PREVIEW|self::PRESAVE|self::POSTSAVE $type */
- public function exec(string $type): void
- {
- if (!in_array($type, [self::PREVIEW, self::PRESAVE, self::POSTSAVE])) {
- throw new InvalidArgumentException('$type must be ArticleSliceAction::PREVIEW, ::PRESAVE or ::POSTSAVE.');
- }
-
- $this->messages = [];
- $this->save = true;
-
- $ga = Sql::factory();
- $ga->setQuery('SELECT a.id, `' . $type . '` as code FROM ' . Core::getTable('module_action') . ' ma,' . Core::getTable('action') . ' a WHERE `' . $type . '` != "" AND ma.action_id=a.id AND module_id=? AND (a.' . $type . 'mode & ?)', [$this->moduleId, $this->mode]);
-
- foreach ($ga as $row) {
- $action = (string) $row->getValue('code');
- $articleId = (int) $row->getValue('id');
- require Stream::factory('action/' . $articleId . '/' . $type, $action);
- }
- }
-
public function addMessage(string $message): void
{
$this->messages[] = $message;
}
/** @param int<1, 20> $index */
- public function setValue(int $index, string $value): void
+ public function setValue(int $index, ?string $value): void
{
$this->sql->setValue('value' . $index, $value);
}
/** @param int<1, 10> $index */
- public function setMedia(int $index, string $value): void
+ public function setMedia(int $index, ?string $value): void
{
$this->sql->setValue('media' . $index, $value);
}
/** @param int<1, 10> $index */
- public function setMediaList(int $index, string $value): void
+ public function setMediaList(int $index, ?string $value): void
{
$this->sql->setValue('medialist' . $index, $value);
}
/** @param int<1, 10> $index */
- public function setLink(int $index, int $value): void
+ public function setLink(int $index, ?int $value): void
{
$this->sql->setValue('link' . $index, $value);
}
/** @param int<1, 10> $index */
- public function setLinkList(int $index, string $value): void
+ public function setLinkList(int $index, ?string $value): void
{
$this->sql->setValue('linklist' . $index, $value);
}
diff --git a/src/Content/AsModule.php b/src/Content/AsModule.php
new file mode 100644
index 0000000000..28f20a2b09
--- /dev/null
+++ b/src/Content/AsModule.php
@@ -0,0 +1,14 @@
+setDebug();
- $sql->setQuery('select clang_id,template_id from ' . Core::getTablePrefix() . 'article where id=? and startarticle=1', [$categoryId]);
+ $sql->setQuery('select clang_id,template from ' . Core::getTablePrefix() . 'article where id=? and startarticle=1', [$categoryId]);
for ($i = 0; $i < $sql->getRows(); $i++, $sql->next()) {
- $startpageTemplates[(int) $sql->getValue('clang_id')] = $sql->getValue('template_id');
+ $startpageTemplates[(int) $sql->getValue('clang_id')] = $sql->getValue('template');
}
}
@@ -79,22 +79,15 @@ public static function addCategory($categoryId, array $data)
// Kategorie in allen Sprachen anlegen
$AART = Sql::factory();
foreach (Language::getAllIds() as $key) {
- $templateId = Template::getDefaultId();
+ $templateKey = Template::getDefaultKey();
if (isset($startpageTemplates[$key]) && '' != $startpageTemplates[$key]) {
- $templateId = $startpageTemplates[$key];
+ $templateKey = $startpageTemplates[$key];
}
// Wenn Template nicht vorhanden, dann entweder erlaubtes nehmen
// oder leer setzen.
- if (!isset($templates[$templateId])) {
- $templateId = 0;
- if (count($templates) > 0) {
- $templateId = key($templates);
- }
- }
-
- if (!isset($templateId)) {
- $templateId = 0;
+ if (null === $templateKey || !isset($templates[$templateKey])) {
+ $templateKey = count($templates) > 0 ? key($templates) : null;
}
$AART->setTable(Core::getTablePrefix() . 'article');
@@ -105,7 +98,7 @@ public static function addCategory($categoryId, array $data)
}
$AART->setValue('clang_id', $key);
- $AART->setValue('template_id', $templateId);
+ $AART->setValue('template', $templateKey);
$AART->setValue('name', $data['name']);
$AART->setValue('catname', $data['catname']);
$AART->setValue('catpriority', $data['catpriority']);
diff --git a/src/Content/ContentHandler.php b/src/Content/ContentHandler.php
index 976dee6845..ec246376c8 100644
--- a/src/Content/ContentHandler.php
+++ b/src/Content/ContentHandler.php
@@ -23,7 +23,7 @@
class ContentHandler
{
/** @throws ApiFunctionException */
- public static function addSlice(int $articleId, int $clangId, int $ctypeId, int $moduleId, array $data = []): string
+ public static function addSlice(int $articleId, int $clangId, int $ctypeId, string $moduleKey, array $data = []): string
{
$data['revision'] ??= 0;
@@ -43,7 +43,7 @@ public static function addSlice(int $articleId, int $clangId, int $ctypeId, int
$sql->setValue('article_id', $articleId);
$sql->setValue('clang_id', $clangId);
$sql->setValue('ctype_id', $ctypeId);
- $sql->setValue('module_id', $moduleId);
+ $sql->setValue('module', $moduleKey);
foreach ($data as $key => $value) {
$sql->setValue($key, $value);
@@ -77,7 +77,7 @@ public static function addSlice(int $articleId, int $clangId, int $ctypeId, int
'page' => Controller::getCurrentPage(),
'ctype' => $ctypeId,
'category_id' => $article->getCategoryId(),
- 'module_id' => $moduleId,
+ 'module_key' => $moduleKey,
'article_revision' => 0,
'slice_revision' => $data['revision'],
]));
diff --git a/src/Content/ContentSection.php b/src/Content/ContentSection.php
index 157f474594..03654c5e5f 100644
--- a/src/Content/ContentSection.php
+++ b/src/Content/ContentSection.php
@@ -2,31 +2,11 @@
namespace Redaxo\Core\Content;
-use Redaxo\Core\Core;
-use Redaxo\Core\Database\Sql;
-
final readonly class ContentSection
{
- private function __construct(
+ public function __construct(
/** @var positive-int */
public int $id,
public string $name,
) {}
-
- /** @return list */
- public static function forTemplate(int $templateId): array
- {
- $sql = Sql::factory();
- $sql->setQuery('SELECT attributes FROM ' . Core::getTable('template') . ' WHERE id = ?', [$templateId]);
- $attributes = $sql->getArrayValue('attributes');
-
- /** @var array $ctypesData */
- $ctypesData = $attributes['ctype'] ?? [];
-
- $ctypes = [];
- foreach ($ctypesData as $id => $name) {
- $ctypes[] = new self($id, $name);
- }
- return $ctypes;
- }
}
diff --git a/src/Content/ExtensionPoint/SliceMenu.php b/src/Content/ExtensionPoint/SliceMenu.php
index 65b980e064..e76ab1ad26 100644
--- a/src/Content/ExtensionPoint/SliceMenu.php
+++ b/src/Content/ExtensionPoint/SliceMenu.php
@@ -33,7 +33,7 @@ public function __construct(
private int $articleId,
private int $clang,
private int $ctype,
- private int $moduleId,
+ private string $moduleKey,
private int $sliceId,
private bool $hasPerm,
) {
@@ -112,7 +112,7 @@ public function getAdditionalActions(): array
'article_id' => $this->articleId,
'clang' => $this->clang,
'ctype' => $this->ctype,
- 'module_id' => $this->moduleId,
+ 'module_key' => $this->moduleKey,
'slice_id' => $this->sliceId,
'perm' => $this->hasPerm,
],
@@ -156,9 +156,9 @@ public function getCtypeId(): int
return $this->ctype;
}
- public function getModuleId(): int
+ public function getModuleKey(): string
{
- return $this->moduleId;
+ return $this->moduleKey;
}
public function getSliceId(): int
diff --git a/src/Content/Module.php b/src/Content/Module.php
index 7bf8ef4fd7..d86b04df5c 100644
--- a/src/Content/Module.php
+++ b/src/Content/Module.php
@@ -2,68 +2,74 @@
namespace Redaxo\Core\Content;
-use Redaxo\Core\Filesystem\File;
+use Redaxo\Core\ClassDiscovery;
-use function assert;
-
-class Module
+abstract class Module
{
- private int $id;
- private ?string $key = '';
-
- public function __construct(int $moduleId)
+ /** @var array|null */
+ private static ?array $instances = null;
+
+ public function __construct(
+ /** Unique key, used as DB reference in rex_article_slice.module. */
+ public readonly string $key,
+ public readonly string $name,
+ ) {}
+
+ /**
+ * Get a module by key or FQCN.
+ *
+ * @param string|class-string $keyOrClass
+ */
+ public static function get(string $keyOrClass): ?self
{
- $this->id = $moduleId;
- }
+ $all = self::getAll();
- public static function forKey(string $moduleKey): ?self
- {
- $mapping = self::getKeyMapping();
-
- if (false !== $id = array_search($moduleKey, $mapping, true)) {
- $module = new self($id);
- $module->key = $moduleKey;
-
- return $module;
+ if (isset($all[$keyOrClass])) {
+ return $all[$keyOrClass];
}
- return null;
+ // FQCN lookup
+ /** @var ?self */
+ return array_find($all, static fn (self $module) => $module instanceof $keyOrClass);
}
- public function getId(): int
+ /**
+ * Check if a module exists by key or FQCN.
+ *
+ * @param string|class-string $keyOrClass
+ */
+ public static function exists(string $keyOrClass): bool
{
- return $this->id;
+ return null !== self::get($keyOrClass);
}
- public function getKey(): ?string
+ /** @return array */
+ public static function getAll(): array
{
- // key will never be empty string in the db
- if ('' === $this->key) {
- $this->key = self::getKeyMapping()[$this->id] ?? null;
- assert('' !== $this->key);
+ if (null !== self::$instances) {
+ return self::$instances;
}
- return $this->key;
- }
+ $instances = [];
+ foreach (ClassDiscovery::getInstance()->discoverByAttribute(AsModule::class, self::class) as $class => $attribute) {
+ $instances[$attribute->key] = new $class($attribute->key, $attribute->name);
+ }
- /** @return array */
- private static function getKeyMapping(): array
- {
- static $mapping;
+ return self::$instances = $instances;
+ }
- if (null !== $mapping) {
- return $mapping;
- }
+ /** Render the backend edit form. */
+ abstract public function input(ArticleSlice $slice): string;
- $file = ModuleCache::getKeyMappingPath();
- $mapping = File::getCache($file, null);
+ /** Render the frontend output. */
+ abstract public function output(ArticleSlice $slice): string;
- if (null !== $mapping) {
- return $mapping;
- }
+ /** Called before the edit form is displayed. */
+ public function onPreview(ArticleSliceAction $action): void {}
- ModuleCache::generateKeyMapping();
+ /** Called before slice data is saved to DB. */
+ public function onPresave(ArticleSliceAction $action): void {}
- return $mapping = File::getCache($file);
- }
+ /** Called after slice data is saved to DB. */
+ public function onPostsave(ArticleSliceAction $action): void {}
}
diff --git a/src/Content/ModuleCache.php b/src/Content/ModuleCache.php
deleted file mode 100644
index 7904c7cbab..0000000000
--- a/src/Content/ModuleCache.php
+++ /dev/null
@@ -1,37 +0,0 @@
-getArray('SELECT id, `key` FROM ' . Core::getTable('module') . ' WHERE `key` IS NOT NULL');
- $mapping = array_column($data, 'key', 'id');
-
- if (!File::putCache(self::getKeyMappingPath(), $mapping)) {
- throw new RuntimeException('Unable to generate module key mapping.');
- }
- }
-
- public static function getKeyMappingPath(): string
- {
- return Path::coreCache('structure/module_key_mapping.cache');
- }
-}
diff --git a/src/Content/ModulePermission.php b/src/Content/ModulePermission.php
index a779a531f9..639ea82ab9 100644
--- a/src/Content/ModulePermission.php
+++ b/src/Content/ModulePermission.php
@@ -2,7 +2,8 @@
namespace Redaxo\Core\Content;
-use Redaxo\Core\Core;
+use Collator;
+use Locale;
use Redaxo\Core\Security\ComplexPermission;
use Redaxo\Core\Translation\I18n;
@@ -10,21 +11,23 @@
class ModulePermission extends ComplexPermission
{
- /**
- * @param int $moduleId
- * @return bool
- */
- public function hasPerm($moduleId)
+ public function hasPerm(string $moduleKey): bool
{
- return $this->hasAll() || in_array($moduleId, $this->perms);
+ return $this->hasAll() || in_array($moduleKey, $this->perms);
}
public static function getFieldParams()
{
+ $options = [];
+ foreach (Module::getAll() as $module) {
+ $options[$module->key] = I18n::translate($module->name);
+ }
+ new Collator(Locale::getDefault())->asort($options);
+
return [
'label' => I18n::msg('modules'),
'all_label' => I18n::msg('all_modules'),
- 'sql_options' => 'select name, id from ' . Core::getTablePrefix() . 'module order by name',
+ 'options' => $options,
];
}
}
diff --git a/src/Content/StructureElement.php b/src/Content/StructureElement.php
index 06f0054797..4b1cf29914 100644
--- a/src/Content/StructureElement.php
+++ b/src/Content/StructureElement.php
@@ -41,8 +41,7 @@ abstract class StructureElement
/** @var string */
protected $catname = '';
- /** @var int */
- protected $template_id = 0;
+ protected ?string $template = null;
/** @var string */
protected $path = '';
@@ -81,7 +80,7 @@ protected function __construct(array $params)
continue;
}
- if (in_array($var, ['id', 'parent_id', 'clang_id', 'template_id', 'priority', 'catpriority', 'status', 'createdate', 'updatedate'], true)) {
+ if (in_array($var, ['id', 'parent_id', 'clang_id', 'priority', 'catpriority', 'status', 'createdate', 'updatedate'], true)) {
$this->$var = (int) $params[$var];
} elseif ('startarticle' === $var) {
$this->$var = (bool) $params[$var];
@@ -391,14 +390,9 @@ public function isOnline()
return 1 == $this->status;
}
- /**
- * Returns the template id.
- *
- * @return int
- */
- public function getTemplateId()
+ public function getTemplateKey(): ?string
{
- return $this->template_id;
+ return $this->template;
}
/**
@@ -408,7 +402,7 @@ public function getTemplateId()
*/
public function hasTemplate()
{
- return $this->template_id > 0;
+ return null !== $this->template;
}
/**
diff --git a/src/Content/Template.php b/src/Content/Template.php
index 4a6a8dfdc0..1e9180eb71 100644
--- a/src/Content/Template.php
+++ b/src/Content/Template.php
@@ -2,246 +2,151 @@
namespace Redaxo\Core\Content;
+use Redaxo\Core\ClassDiscovery;
use Redaxo\Core\Core;
-use Redaxo\Core\Database\Sql;
-use Redaxo\Core\Filesystem\File;
-use Redaxo\Core\Filesystem\Url;
-use Redaxo\Core\Language\Language;
-use Redaxo\Core\Translation\I18n;
-use Redaxo\Core\Util\Stream;
-use Redaxo\Core\Util\Timer;
-
-use function assert;
-use function in_array;
-use function is_array;
-use function Redaxo\Core\View\escape;
-
-class Template
+use Redaxo\Core\Util\Type;
+
+abstract class Template
{
- private int $id;
- private ?string $key = '';
+ /** @var array|null */
+ private static ?array $instances = null;
- public function __construct($templateId)
- {
- $this->id = (int) $templateId;
- }
+ public function __construct(
+ /** Unique key, used as DB reference in rex_article.template. */
+ public readonly string $key,
+ public readonly string $name,
+ ) {}
- /** @return int */
- public static function getDefaultId()
+ public static function getDefaultKey(): ?string
{
- return Core::getConfig('default_template_id', 1);
+ return Type::nullOrString(Core::getConfig('default_template'));
}
- public static function forKey(string $templateKey): ?self
+ /**
+ * Get a template by key or FQCN.
+ *
+ * @param string|class-string $keyOrClass
+ */
+ public static function get(string $keyOrClass): ?self
{
- $mapping = self::getKeyMapping();
-
- if (false !== $id = array_search($templateKey, $mapping, true)) {
- $template = new self($id);
- $template->key = $templateKey;
+ $all = self::getAll();
- return $template;
+ if (isset($all[$keyOrClass])) {
+ return $all[$keyOrClass];
}
- return null;
+ // FQCN lookup
+ /** @var ?self */
+ return array_find($all, static fn (self $template) => $template instanceof $keyOrClass);
}
- /** @return int */
- public function getId()
- {
- return $this->id;
- }
-
- public function getKey(): ?string
+ /**
+ * Check if a template exists by key or FQCN.
+ *
+ * @param string|class-string $keyOrClass
+ */
+ public static function exists(string $keyOrClass): bool
{
- // key will never be empty string in the db
- if ('' === $this->key) {
- $this->key = self::getKeyMapping()[$this->id] ?? null;
- assert('' !== $this->key);
- }
-
- return $this->key;
+ return null !== self::get($keyOrClass);
}
- /** @return false|string */
- public function getFile()
+ /** @return array */
+ public static function getAll(): array
{
- if ($this->getId() < 1) {
- return false;
+ if (null !== self::$instances) {
+ return self::$instances;
}
- $file = TemplateCache::getPath($this->id);
-
- if (!is_file($file)) {
- TemplateCache::generate($this->id);
+ $instances = [];
+ foreach (ClassDiscovery::getInstance()->discoverByAttribute(AsTemplate::class, self::class) as $class => $attribute) {
+ $instances[$attribute->key] = new $class($attribute->key, $attribute->name);
}
- return $file;
+ return self::$instances = $instances;
}
- /** @return false|string|null */
- public function getTemplate()
+ /**
+ * Returns all templates available for the given category.
+ *
+ * @return array
+ */
+ public static function getTemplatesForCategory(?int $categoryId): array
{
- $file = $this->getFile();
- if (!$file) {
- return false;
- }
-
- return File::get($file);
- }
+ $category = $categoryId > 0 ? Category::get($categoryId) : null;
- public function render(): string
- {
- return Timer::measure('Template: ' . ($this->getKey() ?? $this->id), function () {
- return File::getOutput(Stream::factory('template/' . $this->id, $this->getTemplate() ?: ''));
- });
+ return array_filter(
+ self::getAll(),
+ static fn (self $template) => $template->isAllowedInCategory($category),
+ );
}
/**
- * Returns an array containing all templates which are available for the given category_id.
- * if the category_id is non-positive all templates in the system are returned.
- * if the category_id is invalid an empty array is returned.
- *
- * @param int $categoryId
- * @param bool $ignoreInactive
+ * Check if a module is allowed in a given content section of a template (resolves keys to objects).
*
- * @return array
+ * @param positive-int $contentSectionId
+ * @internal
*/
- public static function getTemplatesForCategory($categoryId, $ignoreInactive = true)
+ public static function checkModuleAllowed(string $templateKey, int $contentSectionId, string $moduleKey): bool
{
- $templates = [];
- $tSql = Sql::factory();
- $where = $ignoreInactive ? ' WHERE active=1' : '';
- $tSql->setQuery('select id,name,attributes from ' . Core::getTablePrefix() . 'template' . $where . ' order by name');
-
- if ($categoryId < 1) {
- // Alle globalen Templates
- foreach ($tSql as $row) {
- $attributes = $row->getArrayValue('attributes');
- $categories = $attributes['categories'] ?? [];
- if (!is_array($categories) || (isset($categories['all']) && 1 == $categories['all'])) {
- $templates[(int) $row->getValue('id')] = (string) $row->getValue('name');
- }
- }
- } else {
- if ($c = Category::get($categoryId)) {
- $path = $c->getPathAsArray();
- $path[] = $categoryId;
- foreach ($tSql as $row) {
- $attributes = $row->getArrayValue('attributes');
- $categories = $attributes['categories'] ?? [];
- // template ist nicht kategoriespezifisch -> includen
- if (!is_array($categories) || (isset($categories['all']) && 1 == $categories['all'])) {
- $templates[(int) $row->getValue('id')] = (string) $row->getValue('name');
- } else {
- // template ist auf kategorien beschraenkt..
- // nachschauen ob eine davon im pfad der aktuellen kategorie liegt
- foreach ($path as $p) {
- if (in_array($p, $categories)) {
- $templates[(int) $row->getValue('id')] = (string) $row->getValue('name');
- break;
- }
- }
- }
- }
- }
- }
- return $templates;
- }
+ $template = self::get($templateKey);
- /** @return bool */
- public static function hasModule(array $templateAttributes, $ctype, $moduleId)
- {
- $templateModules = $templateAttributes['modules'] ?? [];
- if (!isset($templateModules[$ctype]['all']) || 1 == $templateModules[$ctype]['all']) {
+ if (null === $template) {
return true;
}
- return is_array($templateModules[$ctype]) && in_array($moduleId, $templateModules[$ctype]);
- }
+ $module = Module::get($moduleKey);
- /** @return array */
- private static function getKeyMapping(): array
- {
- static $mapping;
-
- if (null !== $mapping) {
- return $mapping;
+ if (null === $module) {
+ return false;
}
- $file = TemplateCache::getKeyMappingPath();
- $mapping = File::getCache($file, null);
+ $section = $template->getContentSection($contentSectionId);
- if (null !== $mapping) {
- return $mapping;
+ if (null === $section) {
+ return true;
}
- TemplateCache::generateKeyMapping();
-
- return $mapping = File::getCache($file);
+ return $template->isModuleAllowed($section, $module);
}
- /** @return list */
- public function getCtypes(): array
+ /** @return non-empty-list */
+ public function getContentSections(): array
{
- return ContentSection::forTemplate($this->id);
+ return [new ContentSection(1, 'Content')];
}
- /** @return false|string */
- public static function templateIsInUse(int $templateId, string $msgKey)
+ /**
+ * Whether this template is available in the given category.
+ * `null` means root level (articles without a category).
+ * The default implementation allows the template everywhere.
+ *
+ * Override this to restrict to specific categories.
+ */
+ public function isAllowedInCategory(?Category $category): bool
{
- $check = Sql::factory();
- $check->setQuery('
- SELECT article.id, article.clang_id, template.name
- FROM ' . Core::getTable('article') . ' article
- LEFT JOIN ' . Core::getTable('template') . ' template ON article.template_id=template.id
- WHERE article.template_id=?
- LIMIT 20
- ', [$templateId]);
-
- if (!$check->getRows()) {
- return false;
- }
- $templateInUseMessage = '';
- $error = '';
- $templatename = $check->getRows() ? $check->getValue('template.name') : null;
- while ($check->hasNext()) {
- $aid = (int) $check->getValue('article.id');
- $clangId = (int) $check->getValue('article.clang_id');
- $article = Article::get($aid, $clangId);
- if (null == $article) {
- continue;
- }
- $label = $article->getName() . ' [' . $aid . ']';
- if (Language::count() > 1) {
- $clang = Language::get($clangId);
- if (null == $clang) {
- continue;
- }
- $label .= ' [' . $clang->getCode() . ']';
- }
-
- $templateInUseMessage .= '' . escape($label) . '';
- $check->next();
- }
-
- if (null == $templatename) {
- $check->setQuery('SELECT name FROM ' . Core::getTable('template') . ' WHERE id = ' . $templateId);
- $templatename = $check->getValue('name');
- }
+ return true;
+ }
- if ('' != $templateInUseMessage && null != $templatename) {
- $error .= I18n::msg($msgKey, (string) $templatename);
- $error .= '' . $templateInUseMessage . '
';
- }
+ /**
+ * Whether the given module is allowed in the given content section of this template.
+ * The default implementation allows all modules everywhere.
+ */
+ public function isModuleAllowed(ContentSection $section, Module $module): bool
+ {
+ return true;
+ }
- return $error;
+ /** @param positive-int $id */
+ final public function getContentSection(int $id): ?ContentSection
+ {
+ return array_find($this->getContentSections(), static fn (ContentSection $s) => $s->id === $id);
}
- public static function exists(int $templateId): bool
+ /** @param positive-int $id */
+ final public function hasContentSection(int $id): bool
{
- $sql = Sql::factory();
- $sql->setQuery('SELECT 1 FROM ' . Core::getTable('template') . ' WHERE id = ?', [$templateId]);
- return 1 === $sql->getRows();
+ return null !== $this->getContentSection($id);
}
+
+ abstract public function render(ArticleContent $article): string;
}
diff --git a/src/Content/TemplateCache.php b/src/Content/TemplateCache.php
deleted file mode 100644
index 5d5567639d..0000000000
--- a/src/Content/TemplateCache.php
+++ /dev/null
@@ -1,64 +0,0 @@
-setQuery('SELECT * FROM ' . Core::getTable('template') . ' WHERE id = ?', [$id]);
-
- if (1 !== $sql->getRows()) {
- throw new RuntimeException('Template with id "' . $id . '" does not exist.');
- }
-
- $path = self::getPath($id);
- if (!File::put($path, $sql->getValue('content'))) {
- throw new RuntimeException('Unable to generate template "' . $id . '".');
- }
-
- if (function_exists('opcache_invalidate')) {
- opcache_invalidate($path);
- }
- }
-
- public static function generateKeyMapping(): void
- {
- $data = Sql::factory()->getArray('SELECT id, `key` FROM ' . Core::getTable('template') . ' WHERE `key` IS NOT NULL');
- $mapping = array_column($data, 'key', 'id');
-
- if (!File::putCache(self::getKeyMappingPath(), $mapping)) {
- throw new RuntimeException('Unable to generate template key mapping.');
- }
- }
-
- public static function getPath(int $id): string
- {
- return Path::coreCache('structure/templates/' . $id . '.template');
- }
-
- public static function getKeyMappingPath(): string
- {
- return Path::coreCache('structure/templates/template_key_mapping.cache');
- }
-}
diff --git a/src/Form/Select/TemplateSelect.php b/src/Form/Select/TemplateSelect.php
index 2d21802639..9867ad5847 100644
--- a/src/Form/Select/TemplateSelect.php
+++ b/src/Form/Select/TemplateSelect.php
@@ -2,6 +2,8 @@
namespace Redaxo\Core\Form\Select;
+use Collator;
+use Locale;
use Redaxo\Core\Content\Template;
use Redaxo\Core\Core;
use Redaxo\Core\Database\Sql;
@@ -16,7 +18,7 @@ class TemplateSelect extends Select
private $loaded = false;
/** @var int|null */
private $categoryId;
- /** @var array|null */
+ /** @var array|null */
private $templates;
/** @var int */
private $clangId;
@@ -40,11 +42,14 @@ public function get()
$templates = $this->getTemplates();
if (count($templates) > 0) {
- foreach ($templates as $templateId => $templateName) {
- $this->addOption($templateName, $templateId);
+ $templateNames = array_map(static fn (Template $template) => I18n::translate($template->name), $templates);
+ new Collator(Locale::getDefault())->asort($templateNames);
+
+ foreach ($templateNames as $templateKey => $templateName) {
+ $this->addOption($templateName, $templateKey);
}
} else {
- $this->addOption(I18n::msg('option_no_template'), '0');
+ $this->addOption(I18n::msg('option_no_template'), '');
}
$this->loaded = true;
@@ -58,46 +63,39 @@ public function setSelectedFromStartArticle()
{
$selected = null;
- // Inherit template_id from start article
+ // Inherit template from start article
if ($this->categoryId > 0) {
$sql = Sql::factory();
- $sql->setQuery('SELECT template_id FROM ' . Core::getTable('article') . ' WHERE id = ? AND clang_id = ? AND startarticle = 1', [
+ $sql->setQuery('SELECT template FROM ' . Core::getTable('article') . ' WHERE id = ? AND clang_id = ? AND startarticle = 1', [
$this->categoryId,
$this->clangId,
]);
if (1 == $sql->getRows()) {
- $selected = $sql->getValue('template_id');
+ $selected = $sql->getValue('template');
}
}
$templates = $this->getTemplates();
if (!$selected || !isset($templates[$selected])) {
- $selected = Template::getDefaultId();
+ $selected = Template::getDefaultKey();
}
- if ($selected && isset($templates[$selected])) {
+ if (null !== $selected && isset($templates[$selected])) {
parent::setSelected($selected);
}
}
- /** @return array */
- public function getTemplates()
+ /** @return array */
+ public function getTemplates(): array
{
- if (null === $this->templates) {
- $this->templates = [];
-
- if (null !== $this->categoryId) {
- $templates = Template::getTemplatesForCategory($this->categoryId);
- } else {
- $templates = Sql::factory()->getArray('SELECT id, name FROM ' . Core::getTable('template') . ' WHERE active = 1 ORDER BY name');
- $templates = array_column($templates, 'name', 'id');
- }
+ if (null !== $this->templates) {
+ return $this->templates;
+ }
- foreach ($templates as $templateId => $templateName) {
- $this->templates[$templateId] = I18n::translate($templateName, false);
- }
+ if (null === $this->categoryId) {
+ return $this->templates = Template::getAll();
}
- return $this->templates;
+ return $this->templates = Template::getTemplatesForCategory($this->categoryId);
}
}
diff --git a/src/MetaInfo/Handler/ArticleHandler.php b/src/MetaInfo/Handler/ArticleHandler.php
index dd2d4bc7b6..50430d79d0 100644
--- a/src/MetaInfo/Handler/ArticleHandler.php
+++ b/src/MetaInfo/Handler/ArticleHandler.php
@@ -63,7 +63,7 @@ protected function buildFilterCondition(array $params)
}
}
- $t = ' OR FIND_IN_SET(' . $OOArt->getValue('template_id') . ', `p`.`templates`)';
+ $t = ' OR FIND_IN_SET(' . Sql::factory()->escape($OOArt->getTemplateKey() ?? '') . ', `p`.`templates`)';
$restrictionsCondition = 'AND (`p`.`restrictions` = "" OR `p`.`restrictions` IS NULL ' . $s . ') AND (`p`.`templates` = "" OR `p`.`templates` IS NULL ' . $t . ')';
}