diff --git a/.tools/phpstan/baseline/missingType.iterableValue.php b/.tools/phpstan/baseline/missingType.iterableValue.php
index f87e478d7a..4104585a45 100644
--- a/.tools/phpstan/baseline/missingType.iterableValue.php
+++ b/.tools/phpstan/baseline/missingType.iterableValue.php
@@ -113,16 +113,6 @@
'count' => 1,
'path' => __DIR__ . '/../../../src/Content/StructureContext.php',
];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Content\\StructureElement::__construct() has parameter $params with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Content/StructureElement.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Content\\StructureElement::_hasValue() has parameter $prefixes with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Content/StructureElement.php',
-];
$ignoreErrors[] = [
'rawMessage' => 'Method Redaxo\\Core\\Content\\StructureElement::getUrl() has parameter $params with no value type specified in iterable type array.',
'count' => 1,
@@ -678,31 +668,6 @@
'count' => 1,
'path' => __DIR__ . '/../../../src/View/View.php',
];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Content\\Category@anonymous/tests/Content/CategoryTest.php:175::__construct() has parameter $params with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../tests/Content/CategoryTest.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Tests\\Content\\CategoryTest::createCategories() has parameter $lev1Params with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../tests/Content/CategoryTest.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Tests\\Content\\CategoryTest::createCategories() has parameter $lev2Params with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../tests/Content/CategoryTest.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Tests\\Content\\CategoryTest::createCategories() has parameter $lev3Params with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../tests/Content/CategoryTest.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Tests\\Content\\CategoryTest::createCategory() has parameter $params with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../tests/Content/CategoryTest.php',
-];
$ignoreErrors[] = [
'rawMessage' => 'Method Redaxo\\Core\\Tests\\MediaPool\\MediaPoolTest::testIsAllowedExtension() has parameter $args with no value type specified in iterable type array.',
'count' => 1,
diff --git a/.tools/phpstan/baseline/missingType.parameter.php b/.tools/phpstan/baseline/missingType.parameter.php
index ce9ee21a6a..def4ef2984 100644
--- a/.tools/phpstan/baseline/missingType.parameter.php
+++ b/.tools/phpstan/baseline/missingType.parameter.php
@@ -63,21 +63,6 @@
'count' => 1,
'path' => __DIR__ . '/../../../src/Content/Linkmap/CategoryTree.php',
];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Content\\Linkmap\\CategoryTreeRenderer::formatLi() has parameter $currentCategoryId with no type specified.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Content/Linkmap/CategoryTreeRenderer.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Content\\Linkmap\\CategoryTreeRenderer::formatLi() has parameter $liAttr with no type specified.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Content/Linkmap/CategoryTreeRenderer.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Content\\Linkmap\\CategoryTreeRenderer::formatLi() has parameter $linkAttr with no type specified.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Content/Linkmap/CategoryTreeRenderer.php',
-];
$ignoreErrors[] = [
'rawMessage' => 'Method Redaxo\\Core\\Content\\Linkmap\\CategoryTreeRenderer::getTree() has parameter $categoryId with no type specified.',
'count' => 1,
diff --git a/.tools/psalm/baseline.xml b/.tools/psalm/baseline.xml
index 620c67b88e..a505cf08d8 100644
--- a/.tools/psalm/baseline.xml
+++ b/.tools/psalm/baseline.xml
@@ -1240,8 +1240,6 @@
- getValue('parent_id')]]>
- getValue('parent_id')]]>
@@ -1256,7 +1254,6 @@
getValue('updateuser'))]]>
-
@@ -1275,7 +1272,6 @@
-
@@ -1832,9 +1828,6 @@
getValue('path')]]>
getValue('path')]]>
-
-
-
@@ -1949,7 +1942,6 @@
-
@@ -1966,10 +1958,6 @@
getValue('priority')]]>
getValue('priority')]]>
-
-
-
-
@@ -1999,10 +1987,6 @@
-
-
-
-
@@ -2024,18 +2008,6 @@
-
-
-
-
-
-
-
-
-
- $val]]>
-
-
getContentSections(), static fn (ContentSection $s) => $s->id === $id)]]>
@@ -3474,10 +3446,6 @@
-
-
-
-
@@ -3508,9 +3476,6 @@
getSubject()]]>
getSubject()]]>
-
-
-
@@ -3615,9 +3580,6 @@
-
-
-
@@ -3627,9 +3589,6 @@
-
-
-
@@ -4101,13 +4060,8 @@
-
-
-
-
-
@@ -4180,9 +4134,6 @@
-
-
-
diff --git a/pages/structure/content.metainfo.php b/pages/structure/content.metainfo.php
index 09da3073b3..1368bb6cb1 100644
--- a/pages/structure/content.metainfo.php
+++ b/pages/structure/content.metainfo.php
@@ -45,7 +45,7 @@
$articleIcon = $articleStatusTypes[$status][2];
$structureContext = new StructureContext([
'article_id' => Request::request('article_id', 'int'),
- 'category_id' => $article->getCategoryId(),
+ 'category_id' => $article->categoryId,
'clang_id' => Request::request('clang', 'int'),
]);
@@ -114,7 +114,7 @@
$formElements = [];
$formElements[] = [
'label' => '',
- 'field' => '',
+ 'field' => '',
];
$fragment = new Fragment();
$fragment->setVar('elements', $formElements, false);
diff --git a/pages/structure/content.php b/pages/structure/content.php
index 14ee7d041d..7893e12347 100644
--- a/pages/structure/content.php
+++ b/pages/structure/content.php
@@ -68,8 +68,8 @@
}
// ----- Artikel wurde gefunden - Kategorie holen
-$OOArt = Article::get($articleId, $clang);
-$categoryId = $OOArt->getCategoryId();
+$OOArt = Article::require($articleId, $clang);
+$categoryId = $OOArt->categoryId;
// ----- Request Parameter
$subpage = Controller::getCurrentPagePart(2);
@@ -86,13 +86,13 @@
]);
// ----- Titel anzeigen
-echo View::title(I18n::msg('content') . ': ' . escape($OOArt->getName()), '');
+echo View::title(I18n::msg('content') . ': ' . escape($OOArt->name), '');
// ----- Languages
echo View::clangSwitchAsButtons($context);
// ----- category pfad und rechte
-echo View::structureBreadcrumb($categoryId, $articleId, $clang);
+echo View::structureBreadcrumb($categoryId ?? 0, $articleId, $clang);
// ----- EXTENSION POINT
echo Extension::registerPoint(new ExtensionPoint('STRUCTURE_CONTENT_HEADER', '', [
diff --git a/pages/structure/index.php b/pages/structure/index.php
index 3af1ade6d3..79e1cda949 100644
--- a/pages/structure/index.php
+++ b/pages/structure/index.php
@@ -81,7 +81,7 @@
$catName = I18n::msg('root_level');
$category = Category::get($structureContext->getCategoryId(), $structureContext->getClangId());
if ($category) {
- $catName = $category->getName();
+ $catName = $category->name;
}
$addCategory = '';
diff --git a/pages/structure/linkmap.php b/pages/structure/linkmap.php
index a521c84e3c..9778f047d5 100644
--- a/pages/structure/linkmap.php
+++ b/pages/structure/linkmap.php
@@ -112,8 +112,8 @@ function insertLink(link,name){
if ($category) {
foreach ($category->getParentTree() as $parent) {
$n = [];
- $n['title'] = str_replace(' ', ' ', escape($parent->getName()));
- $n['href'] = $context->getUrl(['category_id' => $parent->getId()]);
+ $n['title'] = str_replace(' ', ' ', escape($parent->name));
+ $n['href'] = $context->getUrl(['category_id' => $parent->id]);
$navigation[] = $n;
}
}
@@ -137,7 +137,7 @@ function insertLink(link,name){
$fragment->setVar('content', $panel, false);
$content[] = $fragment->parse('core/page/section.php');
-$articleList = new ArticleList($context);
+$articleList = new ArticleList();
$panel = $articleList->getList($categoryId);
$fragment = new Fragment();
diff --git a/rector.php b/rector.php
index 015f273e97..afe374640c 100644
--- a/rector.php
+++ b/rector.php
@@ -533,6 +533,10 @@
new MethodCallToPropertyFetch(Cronjob\CronjobExecutor::class, 'getMessage', 'message'),
new MethodCallToPropertyFetch(Cronjob\CronjobExecutor::class, 'hasMessage', 'message'),
+ new MethodCallToPropertyFetch(Content\Article::class, 'getCategoryId', 'categoryId'), // changed from int to ?int
+ new MethodCallToPropertyFetch(Content\Article::class, 'getTemplateKey', 'templateKey'),
+ new MethodCallToPropertyFetch(Content\Article::class, 'hasTemplate', 'templateKey'), // changed from bool to ?string, callers using the bool need manual adjustment
+
new MethodCallToPropertyFetch(Content\ArticleSlice::class, 'getId', 'id'),
new MethodCallToPropertyFetch(Content\ArticleSlice::class, 'getArticleId', 'articleId'),
new MethodCallToPropertyFetch(Content\ArticleSlice::class, 'getClang', 'clangId'),
@@ -548,6 +552,18 @@
new MethodCallToPropertyFetch(Content\ContentSection::class, 'getId', 'id'),
new MethodCallToPropertyFetch(Content\ContentSection::class, 'getName', 'name'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getId', 'id'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getParentId', 'parentId'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getClangId', 'clangId'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getName', 'name'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getPriority', 'priority'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getPath', 'path'), // changed from string to array, callers using the string need manual adjustment
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getPathAsArray', 'path'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getCreateDate', 'createDate'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getUpdateDate', 'updateDate'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getCreateUser', 'createUser'),
+ new MethodCallToPropertyFetch(Content\StructureElement::class, 'getUpdateUser', 'updateUser'),
+
new MethodCallToPropertyFetch(Language\Language::class, 'getId', 'id'),
new MethodCallToPropertyFetch(Language\Language::class, 'getCode', 'code'),
new MethodCallToPropertyFetch(Language\Language::class, 'getName', 'name'),
diff --git a/src/Cache.php b/src/Cache.php
index 2796fb8429..6f346c5578 100644
--- a/src/Cache.php
+++ b/src/Cache.php
@@ -35,7 +35,6 @@ public static function delete(): string
StructureElement::clearInstancePool();
StructureElement::clearInstanceListPool();
- StructureElement::resetClassVars();
if (function_exists('opcache_reset')) {
opcache_reset();
diff --git a/src/Content/ApiFunction/ArticleCopy.php b/src/Content/ApiFunction/ArticleCopy.php
index 4b96a30c43..47c551087c 100644
--- a/src/Content/ApiFunction/ArticleCopy.php
+++ b/src/Content/ApiFunction/ArticleCopy.php
@@ -44,7 +44,7 @@ public function execute(): Result
}
if (
- !$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())
+ !$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)
|| !$user->getComplexPerm('structure')->hasCategoryPerm($categoryCopyIdNew)
) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
diff --git a/src/Content/ApiFunction/ArticleDelete.php b/src/Content/ApiFunction/ArticleDelete.php
index b1cea7a9ab..b8afefe82f 100644
--- a/src/Content/ApiFunction/ArticleDelete.php
+++ b/src/Content/ApiFunction/ArticleDelete.php
@@ -32,7 +32,7 @@ public function execute(): Result
throw new ApiFunctionException('Unable to find article with id "' . $articleId . '"!');
}
- if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())) {
+ if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/ArticleEdit.php b/src/Content/ApiFunction/ArticleEdit.php
index af7025f72d..f4fffa7a32 100644
--- a/src/Content/ApiFunction/ArticleEdit.php
+++ b/src/Content/ApiFunction/ArticleEdit.php
@@ -35,7 +35,7 @@ public function execute(): Result
if (
!$user->getComplexPerm('clang')->hasPerm($clang)
- || !$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())
+ || !$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)
) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/ArticleMove.php b/src/Content/ApiFunction/ArticleMove.php
index 1070b1f376..b4499dab48 100644
--- a/src/Content/ApiFunction/ArticleMove.php
+++ b/src/Content/ApiFunction/ArticleMove.php
@@ -40,7 +40,7 @@ public function execute(): Result
throw new ApiFunctionException('Unable to find category with id "' . $categoryIdNew . '"!');
}
- $categoryId = $article->getCategoryId();
+ $categoryId = $article->categoryId ?? 0;
if (
!$user->getComplexPerm('structure')->hasCategoryPerm($categoryId)
diff --git a/src/Content/ApiFunction/ArticleSliceMove.php b/src/Content/ApiFunction/ArticleSliceMove.php
index cdcbfccf6d..78b3da3f43 100644
--- a/src/Content/ApiFunction/ArticleSliceMove.php
+++ b/src/Content/ApiFunction/ArticleSliceMove.php
@@ -50,7 +50,7 @@ public function execute(): Result
if (
!$user->getComplexPerm('clang')->hasPerm($clang)
- || !$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())
+ || !$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)
|| !$user->getComplexPerm('modules')->hasPerm($moduleKey)
) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
diff --git a/src/Content/ApiFunction/ArticleSliceStatusChange.php b/src/Content/ApiFunction/ArticleSliceStatusChange.php
index 0bf838126f..54db482af9 100644
--- a/src/Content/ApiFunction/ArticleSliceStatusChange.php
+++ b/src/Content/ApiFunction/ArticleSliceStatusChange.php
@@ -35,7 +35,7 @@ public function execute(): Result
if (
!$user->getComplexPerm('clang')->hasPerm($clang)
- || !$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())
+ || !$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)
) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/ArticleStatusChange.php b/src/Content/ApiFunction/ArticleStatusChange.php
index ca3256fda3..3241543977 100644
--- a/src/Content/ApiFunction/ArticleStatusChange.php
+++ b/src/Content/ApiFunction/ArticleStatusChange.php
@@ -36,7 +36,7 @@ public function execute(): Result
if (
!$user->getComplexPerm('clang')->hasPerm($clang)
- || !$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())
+ || !$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)
) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/ArticleToCategory.php b/src/Content/ApiFunction/ArticleToCategory.php
index 2b3c2352e0..5dfba086b8 100644
--- a/src/Content/ApiFunction/ArticleToCategory.php
+++ b/src/Content/ApiFunction/ArticleToCategory.php
@@ -32,7 +32,7 @@ public function execute(): Result
throw new ApiFunctionException('Unable to find article with id "' . $articleId . '"!');
}
- if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())) {
+ if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/ArticleToStartArticle.php b/src/Content/ApiFunction/ArticleToStartArticle.php
index c7f3d193e6..58b6ce2318 100644
--- a/src/Content/ApiFunction/ArticleToStartArticle.php
+++ b/src/Content/ApiFunction/ArticleToStartArticle.php
@@ -32,7 +32,7 @@ public function execute(): Result
throw new ApiFunctionException('Unable to find article with id "' . $articleId . '"!');
}
- if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())) {
+ if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/CategoryToArticle.php b/src/Content/ApiFunction/CategoryToArticle.php
index 2d6d2f2957..73faedea94 100644
--- a/src/Content/ApiFunction/CategoryToArticle.php
+++ b/src/Content/ApiFunction/CategoryToArticle.php
@@ -33,7 +33,7 @@ public function execute(): Result
throw new ApiFunctionException('Unable to find article with id "' . $articleId . '"!');
}
- if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())) {
+ if (!$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/ApiFunction/ContentCopy.php b/src/Content/ApiFunction/ContentCopy.php
index 982e5059ba..4086c29d3f 100644
--- a/src/Content/ApiFunction/ContentCopy.php
+++ b/src/Content/ApiFunction/ContentCopy.php
@@ -38,7 +38,7 @@ public function execute(): Result
if (
!$user->getComplexPerm('clang')->hasPerm($clangA)
|| !$user->getComplexPerm('clang')->hasPerm($clangB)
- || !$user->getComplexPerm('structure')->hasCategoryPerm($article->getCategoryId())
+ || !$user->getComplexPerm('structure')->hasCategoryPerm($article->categoryId)
) {
throw new ApiFunctionException(I18n::msg('no_rights_to_this_function'));
}
diff --git a/src/Content/Article.php b/src/Content/Article.php
index ce80cc0aba..3e3859ca94 100644
--- a/src/Content/Article.php
+++ b/src/Content/Article.php
@@ -2,77 +2,111 @@
namespace Redaxo\Core\Content;
+use Override;
use Redaxo\Core\Core;
use Redaxo\Core\ExtensionPoint\Extension;
use Redaxo\Core\ExtensionPoint\ExtensionPoint;
+use function in_array;
+
/**
- * Object Oriented Framework: Bildet einen Artikel der Struktur ab.
+ * Bildet einen Artikel der Struktur ab.
*/
-class Article extends StructureElement
+final class Article extends StructureElement
{
- /**
- * Return the current article id.
- *
- * @return int
- */
- public static function getCurrentId()
+ protected string $metaInfoPrefix = 'art_';
+
+ public readonly ?int $categoryId;
+ public readonly ?string $templateKey;
+ public readonly bool $startArticle;
+
+ /** @param array $data */
+ private function __construct(array $data)
+ {
+ // strip irrelevant + Category-only fields up front; the rest gets explicitly
+ // pulled into typed properties below, what remains lands in additionalData.
+ unset($data['pid'], $data['catname'], $data['catpriority']);
+ foreach (array_keys($data) as $key) {
+ if (str_starts_with((string) $key, 'cat_')) {
+ unset($data[$key]);
+ }
+ }
+
+ $getAndUnset = static function (string $key) use (&$data): string|int|null {
+ $value = $data[$key] ?? null;
+ unset($data[$key]);
+ return $value;
+ };
+
+ $id = (int) $getAndUnset('id');
+ $parentId = (int) $getAndUnset('parent_id');
+ $startArticle = (bool) $getAndUnset('startarticle');
+ $path = array_map('intval', array_filter(explode('|', (string) $getAndUnset('path'))));
+ // start-articles share the cache row with their category; their DB path
+ // only includes ancestor categories, so add their own id to keep the
+ // semantic "all category ids on the way to this element"
+ if ($startArticle) {
+ $path[] = $id;
+ }
+
+ parent::__construct(
+ id: $id,
+ clangId: (int) $getAndUnset('clang_id'),
+ name: (string) $getAndUnset('name'),
+ priority: (int) $getAndUnset('priority'),
+ path: array_values($path),
+ status: (int) $getAndUnset('status'),
+ createDate: (int) $getAndUnset('createdate'),
+ updateDate: (int) $getAndUnset('updatedate'),
+ createUser: (string) $getAndUnset('createuser'),
+ updateUser: (string) $getAndUnset('updateuser'),
+ additionalData: $data,
+ );
+
+ $this->categoryId = $startArticle ? $id : ($parentId > 0 ? $parentId : null);
+ $this->templateKey = null === ($t = $getAndUnset('template')) ? null : (string) $t;
+ $this->startArticle = $startArticle;
+ }
+
+ /** @param array $data */
+ #[Override]
+ protected static function fromCache(array $data): static
+ {
+ return new self($data);
+ }
+
+ /** Return the current article id. */
+ public static function getCurrentId(): int
{
return Core::getProperty('article_id', 1);
}
- /**
- * Return the current article.
- *
- * @param int $clang
- *
- * @return self|null
- */
- public static function getCurrent($clang = null)
+ /** Return the current article. */
+ public static function getCurrent(?int $clang = null): ?self
{
return self::get(self::getCurrentId(), $clang);
}
- /**
- * Return the site wide start article id.
- *
- * @return int
- */
- public static function getSiteStartArticleId()
+ /** Return the site wide start article id. */
+ public static function getSiteStartArticleId(): int
{
return Core::getProperty('start_article_id', 1);
}
- /**
- * Return the site wide start article.
- *
- * @param int $clang
- *
- * @return self|null
- */
- public static function getSiteStartArticle($clang = null)
+ /** Return the site wide start article. */
+ public static function getSiteStartArticle(?int $clang = null): ?self
{
return self::get(self::getSiteStartArticleId(), $clang);
}
- /**
- * Return the site wide notfound article id.
- *
- * @return int
- */
- public static function getNotfoundArticleId()
+ /** Return the site wide notfound article id. */
+ public static function getNotfoundArticleId(): int
{
return Core::getProperty('notfound_article_id', 1);
}
- /**
- * Return the site wide notfound article.
- *
- * @param int $clang
- *
- * @return self|null
- */
- public static function getNotfoundArticle($clang = null)
+ /** Return the site wide notfound article. */
+ public static function getNotfoundArticle(?int $clang = null): ?self
{
return self::get(self::getNotfoundArticleId(), $clang);
}
@@ -80,81 +114,68 @@ public static function getNotfoundArticle($clang = null)
/**
* Return a list of top-level articles.
*
- * @param bool $ignoreOfflines
- * @param int $clang
- *
* @return list
*/
- public static function getRootArticles($ignoreOfflines = false, $clang = null)
+ public static function getRootArticles(bool $ignoreOfflines = false, ?int $clang = null): array
{
return self::getChildElements(0, 'alist', $ignoreOfflines, $clang);
}
- /**
- * Returns the category id.
- *
- * @return int
- */
- public function getCategoryId()
+ /** Returns the category this article belongs to (for start-articles the category itself). */
+ public function getCategory(): ?Category
{
- return $this->isStartArticle() ? $this->getId() : $this->getParentId();
+ return null === $this->categoryId ? null : Category::get($this->categoryId, $this->clangId);
}
- /**
- * Returns the parent category.
- *
- * @return Category|null
- */
- public function getCategory()
+ #[Override]
+ protected function getParent(): ?Category
{
- return Category::get($this->getCategoryId(), $this->getClangId());
+ $category = $this->getCategory();
+ return $this->startArticle ? $category?->getParent() : $category;
}
- /**
- * Returns the parent object of the article.
- *
- * @return static|null
- */
- public function getParent()
+ /** Returns true if this article is the start-article for its category. */
+ public function isStartArticle(): bool
{
- return self::get($this->parent_id, $this->clang_id);
+ return $this->startArticle;
}
- /**
- * Returns the path of the category/article.
- *
- * @return string
- */
- public function getPath()
+ /** Returns true if this article is the site-wide start article. */
+ public function isSiteStartArticle(): bool
{
- if ($this->isStartArticle()) {
- return $this->path . $this->id . '|';
- }
+ return $this->id === self::getSiteStartArticleId();
+ }
- return $this->path;
+ /** Returns true if this article is the site-wide not-found article. */
+ public function isNotFoundArticle(): bool
+ {
+ return $this->id === self::getNotfoundArticleId();
}
- public function getValue($value)
+ #[Override]
+ public function getValue(string $key): string|int|null
{
- if ('category_id' === $value) {
- // für die CatId hier den Getter verwenden,
- // da dort je nach ArtikelTyp unterscheidungen getroffen werden müssen
- return $this->getCategoryId();
- }
- return parent::getValue($value);
+ $key = strtolower($key);
+
+ return match ($key) {
+ 'category_id' => $this->categoryId,
+ 'template' => $this->templateKey,
+ 'startarticle' => (int) $this->startArticle,
+ default => parent::getValue($key),
+ };
}
- /**
- * @param string $value
- *
- * @return bool
- */
- public static function hasValue($value)
+ #[Override]
+ public function hasValue(string $key): bool
{
- return parent::_hasValue($value, ['art_']);
+ $key = strtolower($key);
+
+ return in_array($key, ['template', 'startarticle', 'category_id'], true)
+ || parent::hasValue($key);
}
- public function isPermitted()
+ #[Override]
+ public function isPermitted(): bool
{
return (bool) Extension::registerPoint(new ExtensionPoint('ART_IS_PERMITTED', true, ['element' => $this]));
}
diff --git a/src/Content/ArticleCache.php b/src/Content/ArticleCache.php
index 2c8be6bc5b..09dd3452ed 100644
--- a/src/Content/ArticleCache.php
+++ b/src/Content/ArticleCache.php
@@ -152,7 +152,7 @@ public static function generateMeta($articleId, $clangId = null)
$clang = $row->getValue('clang_id');
// --------------------------------------------------- Artikelparameter speichern
- $params = ['last_update_stamp' => time()];
+ $params = [];
foreach ($fieldnames as $field) {
$params[$field] = match ($field) {
'createdate', 'updatedate' => $row->getDateTimeValue($field),
diff --git a/src/Content/ArticleContent.php b/src/Content/ArticleContent.php
index 80bf08fe2f..13323f445b 100644
--- a/src/Content/ArticleContent.php
+++ b/src/Content/ArticleContent.php
@@ -61,8 +61,8 @@ public function setArticleId($articleId)
$rexArticle = Article::get($articleId, $this->clang);
if ($rexArticle instanceof Article) {
- $this->category_id = $rexArticle->getCategoryId();
- $this->template = $rexArticle->getTemplateKey();
+ $this->category_id = $rexArticle->categoryId ?? 0;
+ $this->template = $rexArticle->templateKey;
return true;
}
@@ -81,16 +81,16 @@ public function getValue($value)
$value = $this->correctValue($value);
- if (!Article::hasValue($value)) {
- throw new LogicException('Articles do not have the property "' . $value . '"');
- }
-
$article = Article::get($this->article_id, $this->clang);
if (!$article) {
throw new LogicException('Article for id=' . $this->article_id . ' and clang=' . $this->clang . ' does not exist');
}
+ if (!$article->hasValue($value)) {
+ throw new LogicException('Articles do not have the property "' . $value . '"');
+ }
+
return $article->getValue($value);
}
@@ -103,7 +103,7 @@ public function hasValue($value)
$value = $this->correctValue($value);
- return Article::hasValue($value);
+ return Article::get($this->article_id, $this->clang)?->hasValue($value) ?? false;
}
public function getArticle($curctype = -1)
diff --git a/src/Content/ArticleHandler.php b/src/Content/ArticleHandler.php
index f028866a3c..6f684fa8d1 100644
--- a/src/Content/ArticleHandler.php
+++ b/src/Content/ArticleHandler.php
@@ -41,8 +41,7 @@ public static function addArticle(array $data): string
if (!$parent) {
throw new ApiFunctionException('Target category with ID "' . $data['category_id'] . '" does not exist.');
}
- $path = $parent->getPath();
- $path .= $parent->getId() . '|';
+ $path = '|' . implode('|', [...$parent->path, $parent->id]) . '|';
} else {
$path = '|';
}
@@ -68,7 +67,7 @@ public static function addArticle(array $data): string
$categoryName = '';
if ($category) {
- $categoryName = $category->getName();
+ $categoryName = $category->name;
}
$AART->setTable(Core::getTablePrefix() . 'article');
@@ -136,8 +135,8 @@ public static function editArticle(int $articleId, int $clang, array $data): str
throw new ApiFunctionException('Unable to find article with id "' . $articleId . '" and clang "' . $clang . '"!');
}
- $ooArt = Article::get($articleId, $clang);
- $data['category_id'] = $ooArt->getCategoryId();
+ $ooArt = Article::require($articleId, $clang);
+ $data['category_id'] = $ooArt->categoryId;
$templates = Template::getTemplatesForCategory($data['category_id']);
@@ -413,7 +412,7 @@ public static function prevStatus($currentStatus)
/**
* Berechnet die Prios der Artikel in einer Kategorie neu.
*
- * @param int $parentId KategorieId der Kategorie, die erneuert werden soll
+ * @param int|null $parentId KategorieId der Kategorie, die erneuert werden soll
* @param int $clang ClangId der Kategorie, die erneuert werden soll
* @param int $newPrio Neue PrioNr der Kategorie
* @param int $oldPrio Alte PrioNr der Kategorie
@@ -586,8 +585,7 @@ public static function article2startarticle($neuId)
// cat felder sammeln. +
$params = ['path', 'priority', 'catname', 'startarticle', 'catpriority', 'status'];
- $dbFields = StructureElement::getClassVars();
- foreach ($dbFields as $field) {
+ foreach ($alt->getFieldnames() as $field) {
if (str_starts_with($field, 'cat_')) {
$params[] = $field;
}
diff --git a/src/Content/Category.php b/src/Content/Category.php
index cf9ac85d34..70ef987312 100644
--- a/src/Content/Category.php
+++ b/src/Content/Category.php
@@ -2,28 +2,78 @@
namespace Redaxo\Core\Content;
+use Override;
use Redaxo\Core\ExtensionPoint\Extension;
use Redaxo\Core\ExtensionPoint\ExtensionPoint;
-use function assert;
+use function in_array;
/**
- * Object Oriented Framework: Bildet eine Kategorie der Struktur ab.
+ * Bildet eine Kategorie der Struktur ab.
*/
-class Category extends StructureElement
+final class Category extends StructureElement
{
- /**
- * Return the current category.
- *
- * @param int $clang
- *
- * @return self|null
- */
- public static function getCurrent($clang = null)
+ protected string $metaInfoPrefix = 'cat_';
+
+ public readonly ?int $parentId;
+
+ /** @param array $data */
+ private function __construct(array $data)
+ {
+ // strip irrelevant + Article-only fields up front
+ unset(
+ $data['pid'],
+ $data['name'], $data['priority'], $data['template'], $data['startarticle'],
+ );
+ foreach (array_keys($data) as $key) {
+ if (str_starts_with((string) $key, 'art_')) {
+ unset($data[$key]);
+ }
+ }
+
+ $getAndUnset = static function (string $key) use (&$data): string|int|null {
+ $value = $data[$key] ?? null;
+ unset($data[$key]);
+ return $value;
+ };
+
+ $parentId = (int) $getAndUnset('parent_id');
+
+ parent::__construct(
+ id: (int) $getAndUnset('id'),
+ clangId: (int) $getAndUnset('clang_id'),
+ name: (string) $getAndUnset('catname'),
+ priority: (int) $getAndUnset('catpriority'),
+ path: array_values(array_map('intval', array_filter(explode('|', (string) $getAndUnset('path'))))),
+ status: (int) $getAndUnset('status'),
+ createDate: (int) $getAndUnset('createdate'),
+ updateDate: (int) $getAndUnset('updatedate'),
+ createUser: (string) $getAndUnset('createuser'),
+ updateUser: (string) $getAndUnset('updateuser'),
+ additionalData: $data,
+ );
+
+ $this->parentId = $parentId > 0 ? $parentId : null;
+ }
+
+ /** @param array $data */
+ #[Override]
+ protected static function fromCache(array $data): ?static
+ {
+ // categories only exist for start-articles (which share their cache row)
+ if (!$data['startarticle']) {
+ return null;
+ }
+
+ return new self($data);
+ }
+
+ /** Return the current category. */
+ public static function getCurrent(?int $clang = null): ?self
{
$article = Article::getCurrent($clang);
- return $article ? $article->getCategory() : null;
+ return $article?->getCategory();
}
/**
@@ -35,21 +85,13 @@ public static function getCurrent($clang = null)
* all categories with status 0 will be
* excempt from this list!
*
- * @param bool $ignoreOfflines
- * @param int $clang
- *
* @return list
*/
- public static function getRootCategories($ignoreOfflines = false, $clang = null)
+ public static function getRootCategories(bool $ignoreOfflines = false, ?int $clang = null): array
{
return self::getChildElements(0, 'clist', $ignoreOfflines, $clang);
}
- public function getPriority()
- {
- return $this->catpriority;
- }
-
/**
* Return a list of all subcategories.
* Returns an array of Category objects sorted by $priority.
@@ -58,35 +100,24 @@ public function getPriority()
* all categories with status 0 will be
* excempt from this list!
*
- * @param bool $ignoreOfflines
- *
* @return list
*/
- public function getChildren($ignoreOfflines = false)
+ public function getChildren(bool $ignoreOfflines = false): array
{
- return self::getChildElements($this->id, 'clist', $ignoreOfflines, $this->clang_id);
+ return self::getChildElements($this->id, 'clist', $ignoreOfflines, $this->clangId);
}
- /**
- * Returns the parent category.
- *
- * @return static|null
- */
- public function getParent()
+ /** Returns the parent category. */
+ #[Override]
+ public function getParent(): ?self
{
- return self::get($this->parent_id, $this->clang_id);
+ return null === $this->parentId ? null : self::get($this->parentId, $this->clangId);
}
- /**
- * Returns TRUE if this category is the direct
- * parent of the other category.
- *
- * @return bool
- */
- public function isParent(self $otherCat)
+ /** Returns TRUE if this category is the direct parent of the other category. */
+ public function isParent(self $otherCat): bool
{
- return $this->getId() == $otherCat->getParentId()
- && $this->getClangId() == $otherCat->getClangId();
+ return $this->id === $otherCat->parentId && $this->clangId === $otherCat->clangId;
}
/**
@@ -97,58 +128,41 @@ public function isParent(self $otherCat)
* all articles with status 0 will be
* excempt from this list!
*
- * @param bool $ignoreOfflines
- *
* @return list
*/
- public function getArticles($ignoreOfflines = false)
+ public function getArticles(bool $ignoreOfflines = false): array
{
- return Article::getChildElements($this->id, 'alist', $ignoreOfflines, $this->clang_id);
+ return Article::getChildElements($this->id, 'alist', $ignoreOfflines, $this->clangId);
}
- /**
- * Return the start article for this category.
- *
- * @return Article
- */
- public function getStartArticle()
+ /** Return the start article for this category. */
+ public function getStartArticle(): Article
{
- $article = Article::get($this->id, $this->clang_id);
- assert($article instanceof Article);
- return $article;
+ return Article::require($this->id, $this->clangId);
}
- /**
- * Returns the name of the category.
- *
- * @return string
- */
- public function getName()
+ #[Override]
+ public function getValue(string $key): string|int|null
{
- return $this->catname;
+ return match (strtolower($key)) {
+ 'catname' => $this->name,
+ 'catpriority' => $this->priority,
+ 'parent_id' => $this->parentId,
+ default => parent::getValue($key),
+ };
}
- /**
- * Returns the path of the category.
- *
- * @return string
- */
- public function getPath()
+ #[Override]
+ public function hasValue(string $key): bool
{
- return $this->path;
- }
+ $key = strtolower($key);
- /**
- * @param string $value
- *
- * @return bool
- */
- public static function hasValue($value)
- {
- return parent::_hasValue($value, ['cat_']);
+ return in_array($key, ['catname', 'catpriority', 'parent_id'], true)
+ || parent::hasValue($key);
}
- public function isPermitted()
+ #[Override]
+ public function isPermitted(): bool
{
return (bool) Extension::registerPoint(new ExtensionPoint('CAT_IS_PERMITTED', true, ['element' => $this]));
}
diff --git a/src/Content/CategoryHandler.php b/src/Content/CategoryHandler.php
index 5c30e5c43c..aa9281e361 100644
--- a/src/Content/CategoryHandler.php
+++ b/src/Content/CategoryHandler.php
@@ -42,8 +42,7 @@ public static function addCategory($categoryId, array $data)
if (!$parent) {
throw new ApiFunctionException('Target category with ID "' . $categoryId . '" does not exist.');
}
- $path = $parent->getPath();
- $path .= $parent->getId() . '|';
+ $path = '|' . implode('|', [...$parent->path, $parent->id]) . '|';
} else {
$path = '|';
}
diff --git a/src/Content/ContentHandler.php b/src/Content/ContentHandler.php
index 368cbf8684..83a9136464 100644
--- a/src/Content/ContentHandler.php
+++ b/src/Content/ContentHandler.php
@@ -66,7 +66,7 @@ public static function addSlice(int $articleId, int $clangId, int $ctypeId, stri
$message = I18n::msg('slice_added');
- $article = Article::get($articleId, $clangId);
+ $article = Article::require($articleId, $clangId);
// ----- EXTENSION POINT
$message = Extension::registerPoint(new ExtensionPoint('SLICE_ADDED', $message, [
@@ -76,7 +76,7 @@ public static function addSlice(int $articleId, int $clangId, int $ctypeId, stri
'slice_id' => $sliceId,
'page' => Controller::getCurrentPage(),
'ctype' => $ctypeId,
- 'category_id' => $article->getCategoryId(),
+ 'category_id' => $article->categoryId,
'module_key' => $moduleKey,
'article_revision' => 0,
'slice_revision' => $data['revision'],
@@ -218,14 +218,14 @@ public static function sliceStatus(int $sliceId, int $status)
throw new RuntimeException(sprintf('Slice with id=%d not found.', $sliceId));
}
- $article = Article::get($sql->getValue('article_id'), $sql->getValue('clang_id'));
+ $article = Article::require($sql->getValue('article_id'), $sql->getValue('clang_id'));
$sql->setTable(Core::getTable('article_slice'));
$sql->setWhere(['id' => $sliceId]);
$sql->setValue('status', $status);
$sql->update();
- ArticleCache::deleteContent($article->getId(), $article->getClangId());
+ ArticleCache::deleteContent($article->id, $article->clangId);
Extension::registerPoint(new ArticleContentUpdated($article, 'slice_status'));
}
@@ -335,7 +335,7 @@ public static function copyContent($fromId, $toId, $fromClang = 1, $toClang = 1,
ArticleCache::deleteContent($toId, $toClang);
- $article = Article::get($toId, $toClang);
+ $article = Article::require($toId, $toClang);
Extension::registerPoint(new ArticleContentUpdated($article, 'content_copied'));
return true;
diff --git a/src/Content/ExtensionPoint/ArticleContentUpdated.php b/src/Content/ExtensionPoint/ArticleContentUpdated.php
index d623b651b1..8d0356419a 100644
--- a/src/Content/ExtensionPoint/ArticleContentUpdated.php
+++ b/src/Content/ExtensionPoint/ArticleContentUpdated.php
@@ -19,8 +19,8 @@ class ArticleContentUpdated extends ExtensionPoint
public function __construct(Article $article, string $action, string $subject = '', array $params = [], bool $readonly = false)
{
// for BC 'simple' attach params
- $params['article_id'] = $article->getId();
- $params['clang'] = $article->getClangId();
+ $params['article_id'] = $article->id;
+ $params['clang'] = $article->clangId;
parent::__construct(self::NAME, $subject, $params, $readonly);
diff --git a/src/Content/Linkmap/ArticleList.php b/src/Content/Linkmap/ArticleList.php
index 817bd810ce..cc2294b411 100644
--- a/src/Content/Linkmap/ArticleList.php
+++ b/src/Content/Linkmap/ArticleList.php
@@ -3,7 +3,6 @@
namespace Redaxo\Core\Content\Linkmap;
use Redaxo\Core\Content\Article;
-use Redaxo\Core\Http\Context;
use function Redaxo\Core\View\escape;
use function sprintf;
@@ -13,15 +12,26 @@
*/
class ArticleList extends ArticleListRenderer
{
- public function __construct(
- private Context $context,
- ) {}
-
/** @return string */
protected function listItem(Article $article, $categoryId)
{
- $liAttr = ' class="list-group-item rex-linkmap-list-item-article"';
- $url = 'javascript:insertLink(\'redaxo://' . $article->getId() . '\',\'' . escape(trim(sprintf('%s [%s]', $article->getName(), $article->getId())), 'js') . '\');';
- return CategoryTreeRenderer::formatLi($article, $categoryId, $this->context, $liAttr, ' href="' . $url . '"') . '' . "\n";
+ $url = 'javascript:insertLink(\'redaxo://' . $article->id . '\',\'' . escape(trim(sprintf('%s [%s]', $article->name, $article->id)), 'js') . '\');';
+
+ $linkClass = $article->isOnline() ? 'rex-online' : 'rex-offline';
+ $label = CategoryTreeRenderer::formatLabel($article);
+
+ $iconType = match (true) {
+ $article->isSiteStartArticle() => 'sitestartarticle',
+ $article->isStartArticle() => 'startarticle',
+ default => 'article',
+ };
+
+ return ''
+ . ''
+ . ' '
+ . escape($label)
+ . '' . $article->id . ''
+ . ''
+ . '' . "\n";
}
}
diff --git a/src/Content/Linkmap/CategoryTree.php b/src/Content/Linkmap/CategoryTree.php
index 40ddebaf4b..8fc3656c63 100644
--- a/src/Content/Linkmap/CategoryTree.php
+++ b/src/Content/Linkmap/CategoryTree.php
@@ -34,7 +34,7 @@ protected function treeItem(Category $cat, $liClasses, $linkClasses, $subHtml, $
$badgeCat = ($countChildren > 0) ? '' . $countChildren . '' : '';
$li = '';
$li .= '';
- $li .= '' . $liIcon . escape($label) . '' . $cat->getId() . '';
+ $li .= '' . $liIcon . escape($label) . '' . $cat->id . '';
$li .= $badgeCat;
$li .= $subHtml;
$li .= '' . "\n";
diff --git a/src/Content/Linkmap/CategoryTreeRenderer.php b/src/Content/Linkmap/CategoryTreeRenderer.php
index 0303efad4d..d046af44c1 100644
--- a/src/Content/Linkmap/CategoryTreeRenderer.php
+++ b/src/Content/Linkmap/CategoryTreeRenderer.php
@@ -6,12 +6,10 @@
use Redaxo\Core\Content\Category;
use Redaxo\Core\Content\StructureElement;
use Redaxo\Core\Core;
-use Redaxo\Core\Http\Context;
use Redaxo\Core\Translation\I18n;
use function count;
use function in_array;
-use function Redaxo\Core\View\escape;
/**
* @internal
@@ -39,7 +37,7 @@ public function getTree($categoryId)
$tree = [];
if ($category) {
foreach ($category->getParentTree() as $cat) {
- $tree[] = $cat->getId();
+ $tree[] = $cat->id;
}
}
@@ -62,7 +60,7 @@ public function renderTree(array $children, array $activeTreeIds)
$li = '';
foreach ($children as $cat) {
$catChildren = $cat->getChildren();
- $catId = $cat->getId();
+ $catId = $cat->id;
$liclasses = 'list-group-item';
$linkclasses = '';
$subLi = '';
@@ -79,7 +77,7 @@ public function renderTree(array $children, array $activeTreeIds)
}
if ('' != $li) {
- $ul = '' . "\n";
+ $ul = '' . "\n";
}
return $ul;
@@ -88,35 +86,18 @@ public function renderTree(array $children, array $activeTreeIds)
/** @return string */
abstract protected function treeItem(Category $cat, $liClasses, $linkClasses, $subHtml, $liIcon);
- /** @return string */
- public static function formatLabel(StructureElement $OOobject)
+ public static function formatLabel(StructureElement $OOobject): string
{
- $label = $OOobject->getName();
+ $label = $OOobject->name;
- if ('' == trim($label)) {
+ if ('' === trim($label)) {
$label = ' ';
}
- if ($OOobject instanceof Article && !$OOobject->hasTemplate()) {
+ if ($OOobject instanceof Article && null === $OOobject->templateKey) {
$label .= ' [' . I18n::msg('linkmap_has_no_template') . ']';
}
return $label;
}
-
- /** @return string */
- public static function formatLi(StructureElement $OOobject, $currentCategoryId, Context $context, $liAttr = '', $linkAttr = '')
- {
- $linkAttr .= ' class="' . ($OOobject->isOnline() ? 'rex-online' : 'rex-offline') . '"';
-
- if (!str_contains($linkAttr, ' href=')) {
- $linkAttr .= ' href="' . $context->getUrl(['category_id' => $OOobject->getId()]) . '"';
- }
-
- $label = self::formatLabel($OOobject);
-
- $icon = '';
-
- return '' . $icon . ' ' . escape($label) . '' . $OOobject->getId() . '';
- }
}
diff --git a/src/Content/StructureElement.php b/src/Content/StructureElement.php
index 833839e9df..5844088674 100644
--- a/src/Content/StructureElement.php
+++ b/src/Content/StructureElement.php
@@ -2,172 +2,45 @@
namespace Redaxo\Core\Content;
-use AllowDynamicProperties;
use Redaxo\Core\Base\InstanceListPoolTrait;
use Redaxo\Core\Base\InstancePoolTrait;
-use Redaxo\Core\Core;
-use Redaxo\Core\Database\Sql;
use Redaxo\Core\Exception\LogicException;
+use Redaxo\Core\Exception\RuntimeException;
use Redaxo\Core\Filesystem\File;
use Redaxo\Core\Filesystem\Path;
use Redaxo\Core\Filesystem\Url;
use Redaxo\Core\Language\Language;
+use function array_key_exists;
use function in_array;
+use function sprintf;
/**
- * Object Oriented Framework: Basisklasse für die Strukturkomponenten.
+ * Basisklasse für die Strukturkomponenten.
*/
-#[AllowDynamicProperties]
abstract class StructureElement
{
use InstanceListPoolTrait;
use InstancePoolTrait;
- /** @var int */
- protected $id = 0;
-
- /** @var int */
- protected $parent_id = 0;
-
- /** @var int */
- protected $clang_id = 0;
-
- /** @var string */
- protected $name = '';
-
- /** @var string */
- protected $catname = '';
-
- protected ?string $template = null;
-
- /** @var string */
- protected $path = '';
-
- /** @var int */
- protected $priority = 0;
-
- /** @var int */
- protected $catpriority = 0;
-
- /** @var bool */
- protected $startarticle = false;
-
- /** @var int */
- protected $status = 0;
-
- /** @var int */
- protected $updatedate = 0;
-
- /** @var int */
- protected $createdate = 0;
-
- /** @var string */
- protected $updateuser = '';
-
- /** @var string */
- protected $createuser = '';
-
- /** @var list|null */
- protected static $classVars;
-
- protected function __construct(array $params)
- {
- foreach (self::getClassVars() as $var) {
- if (!isset($params[$var])) {
- continue;
- }
-
- 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];
- } else {
- $this->$var = $params[$var];
- }
- }
- }
-
- /**
- * Returns Object Value.
- *
- * @param string $value
- * @return string|int|null
- * @psalm-taint-source input
- */
- public function getValue($value)
- {
- // damit alte rex_article felder wie teaser, online_from etc
- // noch funktionieren
- // gleicher BC code nochmals in article::getValue
- foreach (['', 'art_', 'cat_'] as $prefix) {
- $val = $prefix . $value;
- if (isset($this->$val)) {
- return $this->$val;
- }
- }
- return null;
- }
-
- /**
- * @param string $value
- *
- * @return bool
- */
- protected static function _hasValue($value, array $prefixes = [])
- {
- $values = self::getClassVars();
-
- if (in_array($value, $values)) {
- return true;
- }
-
- foreach ($prefixes as $prefix) {
- if (in_array($prefix . $value, $values)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Returns an Array containing article field names.
- *
- * @return list
- */
- public static function getClassVars()
- {
- if (empty(self::$classVars)) {
- self::$classVars = [];
-
- $startId = Article::getSiteStartArticleId();
- $file = Path::coreCache('structure/' . $startId . '.1.article');
- if (!Core::isBackend() && is_file($file)) {
- // da getClassVars() eine statische Methode ist, können wir hier nicht mit $this->getId() arbeiten!
- $genVars = File::getCache($file, []);
- unset($genVars['last_update_stamp']);
- foreach ($genVars as $name => $value) {
- self::$classVars[] = (string) $name;
- }
- } else {
- // Im Backend die Spalten aus der DB auslesen / via EP holen
- $sql = Sql::factory();
- $sql->setQuery('SELECT * FROM ' . Core::getTablePrefix() . 'article LIMIT 0');
- foreach ($sql->getFieldnames() as $field) {
- self::$classVars[] = $field;
- }
- }
- }
-
- return self::$classVars;
- }
-
- /** @return void */
- public static function resetClassVars()
- {
- self::$classVars = null;
- }
+ /** Prefix used for meta-info fields of this element type (`art_` or `cat_`). */
+ abstract protected string $metaInfoPrefix { get; }
+
+ protected function __construct(
+ public readonly int $id,
+ public readonly int $clangId,
+ public readonly string $name,
+ public readonly int $priority,
+ /** @var list */
+ public readonly array $path,
+ public readonly int $status,
+ public readonly int $createDate,
+ public readonly int $updateDate,
+ public readonly string $createUser,
+ public readonly string $updateUser,
+ /** @var array */
+ final protected readonly array $additionalData,
+ ) {}
/**
* Return a StructureElement object based on an id.
@@ -188,7 +61,7 @@ public static function get(int $id, ?int $clang = null): ?static
$clang = Language::getCurrentId();
}
- return static::getInstance([$id, $clang], static function () use ($id, $clang) {
+ return static::getInstance([$id, $clang], static function () use ($id, $clang): ?static {
$articlePath = Path::coreCache('structure/' . $id . '.' . $clang . '.article');
// load metadata from cache
@@ -205,17 +78,31 @@ public static function get(int $id, ?int $clang = null): ?static
return null;
}
- // don't allow to retrieve non-categories (startarticle=0) as Category
- if (!$metadata['startarticle'] && (Category::class === static::class || is_subclass_of(static::class, Category::class))) {
- return null;
- }
-
- return new static($metadata);
+ /** @var array $metadata */
+ return static::fromCache($metadata);
});
}
+ /**
+ * Returns the element for the given id like {@see get()}, but throws when it does not exist.
+ *
+ * @throws RuntimeException if no element exists for the given id and clang
+ */
+ public static function require(int $id, ?int $clang = null): static
+ {
+ return static::get($id, $clang)
+ ?? throw new RuntimeException(sprintf('Required %s with id "%d" and clang "%s" does not exist.', static::class, $id, $clang ?? Language::getCurrentId()));
+ }
+
+ /**
+ * Builds an instance from the cache row.
+ *
+ * @param array $data
+ */
+ abstract protected static function fromCache(array $data): ?static;
+
/** @return list */
- protected static function getChildElements(int $parentId, string $listType, bool $ignoreOfflines = false, ?int $clang = null): array
+ final protected static function getChildElements(int $parentId, string $listType, bool $ignoreOfflines = false, ?int $clang = null): array
{
// for $parentId=0 root elements will be returned, so abort here for $parentId<0 only
if (0 > $parentId) {
@@ -252,208 +139,88 @@ static function () use ($parentId, $listType) {
);
}
- /**
- * Returns the clang of the category.
- *
- * @return int
- */
- public function getClangId()
- {
- return $this->clang_id;
- }
-
- /**
- * Returns a url for linking to this article.
- *
- * @return string
- */
- public function getUrl(array $params = [])
+ /** @psalm-taint-source input */
+ public function getValue(string $key): string|int|null
{
- return Url::article($this->getId(), $this->getClangId(), $params);
+ $key = strtolower($key);
+
+ return match ($key) {
+ 'id' => $this->id,
+ 'clang_id' => $this->clangId,
+ 'name' => $this->name,
+ 'priority' => $this->priority,
+ 'path' => $this->path ? '|' . implode('|', $this->path) . '|' : '|',
+ 'status' => $this->status,
+ 'createdate' => $this->createDate,
+ 'updatedate' => $this->updateDate,
+ 'createuser' => $this->createUser,
+ 'updateuser' => $this->updateUser,
+ default => $this->additionalData[$key] ?? $this->additionalData[$this->metaInfoPrefix . $key] ?? null,
+ };
}
- /**
- * Returns the id of the article.
- *
- * @return int
- */
- public function getId()
+ /** Checks whether this element has a value for the given key. */
+ public function hasValue(string $key): bool
{
- return $this->id;
+ $key = strtolower($key);
+
+ return in_array($key, [
+ 'id', 'clang_id', 'name', 'priority', 'path', 'status',
+ 'createdate', 'updatedate', 'createuser', 'updateuser',
+ ], true)
+ || array_key_exists($key, $this->additionalData)
+ || array_key_exists($this->metaInfoPrefix . $key, $this->additionalData);
}
- /**
- * Returns the parent_id of the article.
- *
- * @return int
- */
- public function getParentId()
+ /** Returns a url for linking to this article. */
+ public function getUrl(array $params = []): string
{
- return $this->parent_id;
+ return Url::article($this->id, $this->clangId, $params);
}
/**
- * Returns the path of the category/article.
- *
- * @return string
+ * Returns the next category upwards in the structure tree
+ * (the containing category for articles, the parent category for categories).
*/
- abstract public function getPath();
+ abstract protected function getParent(): ?Category;
- /**
- * Returns the path ids of the category/article as an array.
- *
- * @return list
- */
- public function getPathAsArray()
+ /** Returns true if article is online. */
+ public function isOnline(): bool
{
- $path = explode('|', $this->getPath());
- return array_values(array_map('intval', array_filter($path)));
+ return 1 === $this->status;
}
- /**
- * Returns the parent category.
- *
- * @return static|null
- */
- abstract public function getParent();
-
- /**
- * Returns the name of the article.
- *
- * @return string
- * @psalm-taint-source input
- */
- public function getName()
- {
- return $this->name;
- }
-
- /**
- * Returns the article priority.
- *
- * @return int
- */
- public function getPriority()
- {
- return $this->priority;
- }
-
- /**
- * Returns the last update user.
- *
- * @return string
- */
- public function getUpdateUser()
- {
- return $this->updateuser;
- }
-
- /**
- * Returns the last update date.
- *
- * @return int
- */
- public function getUpdateDate()
- {
- return $this->updatedate;
- }
-
- /**
- * Returns the creator.
- *
- * @return string
- */
- public function getCreateUser()
- {
- return $this->createuser;
- }
-
- /**
- * Returns the creation date.
- *
- * @return int
- */
- public function getCreateDate()
- {
- return $this->createdate;
- }
-
- /**
- * Returns true if article is online.
- *
- * @return bool
- */
- public function isOnline()
- {
- return 1 == $this->status;
- }
-
- public function getTemplateKey(): ?string
- {
- return $this->template;
- }
-
- /**
- * Returns true if article has a template.
- *
- * @return bool
- */
- public function hasTemplate()
- {
- return null !== $this->template;
- }
-
- /**
- * Returns whether the element is permitted.
- *
- * @return bool
- */
- abstract public function isPermitted();
+ /** Returns whether the element is permitted. */
+ abstract public function isPermitted(): bool;
/**
- * Get an array of all parentCategories.
- * Returns an array of StructureElement objects.
+ * Get an array of all parent categories.
*
* @return list
*/
- public function getParentTree()
+ public function getParentTree(): array
{
$return = [];
- if ($this->path) {
- if ($this->isStartArticle()) {
- $explode = explode('|', $this->path . $this->id . '|');
- } else {
- $explode = explode('|', $this->path);
- }
-
- foreach ($explode as $var) {
- if ('' != $var) {
- $cat = Category::get((int) $var, $this->clang_id);
- if (!$cat) {
- throw new LogicException('No category found with id=' . $var . ' and clang=' . $this->clang_id . '.');
- }
- $return[] = $cat;
- }
+ foreach ($this->path as $id) {
+ $cat = Category::get($id, $this->clangId);
+ if (!$cat) {
+ throw new LogicException('No category found with id=' . $id . ' and clang=' . $this->clangId . '.');
}
+ $return[] = $cat;
}
return $return;
}
- /**
- * Checks if $anObj is in the parent tree of the object.
- *
- * @return bool
- */
- public function inParentTree(self $anObj)
+ /** Checks if $anObj is in the parent tree of the object. */
+ public function inParentTree(self $anObj): bool
{
- $tree = $this->getParentTree();
- return in_array($anObj, $tree);
+ return in_array($anObj, $this->getParentTree(), true);
}
/**
- * Returns the closest element from parent tree (including itself) where the callback returns true.
+ * Returns the closest element (this element or any parent category) where the callback returns true.
*
* @param callable(self):bool $callback
*/
@@ -463,17 +230,11 @@ public function getClosest(callable $callback): ?self
return $this;
}
- $parent = $this->getParent();
-
- return $parent ? $parent->getClosest($callback) : null;
+ return $this->getParent()?->getClosest($callback);
}
- /**
- * Returns the value from this element or from the closest parent where the value is set.
- *
- * @return string|int|null
- */
- public function getClosestValue(string $key)
+ /** Returns the value from this element or from the closest parent category where the value is set. */
+ public function getClosestValue(string $key): string|int|null
{
$value = $this->getValue($key);
@@ -481,12 +242,10 @@ public function getClosestValue(string $key)
return $value;
}
- $parent = $this->getParent();
-
- return $parent ? $parent->getClosestValue($key) : null;
+ return $this->getParent()?->getClosestValue($key);
}
- /** Returns true if this element and all parents are online. */
+ /** Returns true if this element and all parent categories are online. */
public function isOnlineIncludingParents(): bool
{
if (!$this->isOnline()) {
@@ -497,34 +256,4 @@ public function isOnlineIncludingParents(): bool
return !$parent || $parent->isOnlineIncludingParents();
}
-
- /**
- * Returns true if this Article is the Startpage for the category.
- *
- * @return bool
- */
- public function isStartArticle()
- {
- return $this->startarticle;
- }
-
- /**
- * Returns true if this Article is the Startpage for the entire site.
- *
- * @return bool
- */
- public function isSiteStartArticle()
- {
- return $this->id == Article::getSiteStartArticleId();
- }
-
- /**
- * Returns true if this Article is the not found article.
- *
- * @return bool
- */
- public function isNotFoundArticle()
- {
- return $this->id == Article::getNotfoundArticleId();
- }
}
diff --git a/src/Content/StructurePermission.php b/src/Content/StructurePermission.php
index df8cebcfbf..6e17aa3e44 100644
--- a/src/Content/StructurePermission.php
+++ b/src/Content/StructurePermission.php
@@ -12,14 +12,20 @@
/** @extends ComplexPermission */
final class StructurePermission extends ComplexPermission
{
- public function hasCategoryPerm(int $categoryId): bool
+ public function hasCategoryPerm(?int $categoryId): bool
{
- if ($this->hasAll() || in_array($categoryId, $this->perms, true)) {
+ if ($this->hasAll()) {
+ return true;
+ }
+ if (null === $categoryId) {
+ return false;
+ }
+ if (in_array($categoryId, $this->perms, true)) {
return true;
}
if ($c = Category::get($categoryId)) {
$perms = $this->perms;
- return array_any($c->getPathAsArray(), static fn (int $k) => in_array($k, $perms, true));
+ return array_any($c->path, static fn (int $k) => in_array($k, $perms, true));
}
return false;
}
@@ -56,16 +62,16 @@ public function getMountpointCategories(): array
}
$categories[] = $category;
- $parents[$category->getParentId()] = true;
+ $parents[$category->parentId ?? 0] = true;
}
if (count($parents) <= 1) {
usort($categories, static function (Category $a, Category $b) {
- return $a->getPriority() <=> $b->getPriority();
+ return $a->priority <=> $b->priority;
});
} else {
usort($categories, static function (Category $a, Category $b) {
- return strcasecmp($a->getName(), $b->getName());
+ return strcasecmp($a->name, $b->name);
});
}
diff --git a/src/Form/Select/CategorySelect.php b/src/Form/Select/CategorySelect.php
index 34f3a4fbfd..7e6e74735b 100644
--- a/src/Form/Select/CategorySelect.php
+++ b/src/Form/Select/CategorySelect.php
@@ -88,21 +88,18 @@ protected function addCatOptions()
/** @return void */
protected function addCatOption(Category $cat, $group = null)
{
- if (!$this->checkPerms || Core::requireUser()->getComplexPerm('structure')->hasCategoryPerm($cat->getId())
+ if (!$this->checkPerms || Core::requireUser()->getComplexPerm('structure')->hasCategoryPerm($cat->id)
) {
- $cid = $cat->getId();
- $cname = $cat->getName() . ' [' . $cid . ']';
+ $cid = $cat->id;
+ $cname = $cat->name . ' [' . $cid . ']';
if (null === $group) {
- $group = $cat->getParentId();
+ $group = $cat->parentId ?? 0;
}
$this->addOption($cname, $cid, $cid, $group);
- $childs = $cat->getChildren($this->ignoreOfflines);
- if (is_array($childs)) {
- foreach ($childs as $child) {
- $this->addCatOption($child);
- }
+ foreach ($cat->getChildren($this->ignoreOfflines) as $child) {
+ $this->addCatOption($child);
}
}
}
diff --git a/src/MediaPool/MediaPool.php b/src/MediaPool/MediaPool.php
index a6a849c08e..52c4a4f894 100644
--- a/src/MediaPool/MediaPool.php
+++ b/src/MediaPool/MediaPool.php
@@ -91,8 +91,8 @@ public static function mediaIsInUse(string $filename): string|false
foreach ($res as $artArr) {
$aid = (int) $artArr['article_id'];
$clang = (int) $artArr['clang_id'];
- $ooa = Article::get($aid, $clang);
- $name = ($ooa) ? $ooa->getName() : '';
+ $article = Article::get($aid, $clang);
+ $name = $article ? $article->name : '';
$warning[0] .= '' . $name . '';
}
$warning[0] .= '';
diff --git a/src/MetaInfo/Handler/ArticleHandler.php b/src/MetaInfo/Handler/ArticleHandler.php
index 50430d79d0..124a3f5392 100644
--- a/src/MetaInfo/Handler/ArticleHandler.php
+++ b/src/MetaInfo/Handler/ArticleHandler.php
@@ -54,16 +54,14 @@ protected function buildFilterCondition(array $params)
if (!empty($params['id'])) {
$s = '';
- $OOArt = Article::get($params['id'], $params['clang']);
+ $OOArt = Article::require($params['id'], $params['clang']);
// Alle Metafelder des Pfades sind erlaubt
- foreach ($OOArt->getPathAsArray() as $pathElement) {
- if ('' != $pathElement) {
- $s .= ' OR FIND_IN_SET(' . $pathElement . ', `p`.`restrictions`)';
- }
+ foreach ($OOArt->path as $pathElement) {
+ $s .= ' OR FIND_IN_SET(' . $pathElement . ', `p`.`restrictions`)';
}
- $t = ' OR FIND_IN_SET(' . Sql::factory()->escape($OOArt->getTemplateKey() ?? '') . ', `p`.`templates`)';
+ $t = ' OR FIND_IN_SET(' . Sql::factory()->escape($OOArt->templateKey ?? '') . ', `p`.`templates`)';
$restrictionsCondition = 'AND (`p`.`restrictions` = "" OR `p`.`restrictions` IS NULL ' . $s . ') AND (`p`.`templates` = "" OR `p`.`templates` IS NULL ' . $t . ')';
}
@@ -83,7 +81,7 @@ public function getForm(array $params)
$params['activeItem'] = $params['article'];
// Hier die category_id setzen, damit beim klick auf den REX_LINK_BUTTON der Medienpool in der aktuellen Kategorie startet
- $params['activeItem']->setValue('category_id', $OOArt->getCategoryId());
+ $params['activeItem']->setValue('category_id', $OOArt->categoryId ?? 0);
return parent::renderFormAndSave(self::PREFIX, $params);
}
diff --git a/src/MetaInfo/Handler/CategoryHandler.php b/src/MetaInfo/Handler/CategoryHandler.php
index 874ee45de2..45c2f64b7e 100644
--- a/src/MetaInfo/Handler/CategoryHandler.php
+++ b/src/MetaInfo/Handler/CategoryHandler.php
@@ -64,13 +64,11 @@ protected function buildFilterCondition(array $params)
$s = '';
if (!empty($params['id'])) {
- $OOCat = Category::get($params['id'], $params['clang']);
+ $OOCat = Category::require($params['id'], $params['clang']);
// Alle Metafelder des Pfades sind erlaubt
- foreach ($OOCat->getPathAsArray() as $pathElement) {
- if ('' != $pathElement) {
- $s .= ' OR FIND_IN_SET(' . $pathElement . ', `p`.`restrictions`)';
- }
+ foreach ($OOCat->path as $pathElement) {
+ $s .= ' OR FIND_IN_SET(' . $pathElement . ', `p`.`restrictions`)';
}
// Auch die Kategorie selbst kann Metafelder haben
diff --git a/src/RexVar/LinkListVar.php b/src/RexVar/LinkListVar.php
index dab4c307ca..a9627753cf 100644
--- a/src/RexVar/LinkListVar.php
+++ b/src/RexVar/LinkListVar.php
@@ -20,7 +20,7 @@
*/
public static function getWidget($id, $name, $value, array $args = [])
{
- $category = Category::getCurrent() ? Category::getCurrent()->getId() : 0; // Aktuelle Kategorie vorauswählen
+ $category = Category::getCurrent()->id ?? 0; // Aktuelle Kategorie vorauswählen
// Falls ein Kategorie-Parameter angegeben wurde, die Linkmap in dieser Kategorie öffnen
if (isset($args['category'])) {
@@ -36,7 +36,7 @@ public static function getWidget($id, $name, $value, array $args = [])
continue;
}
if ($article = Article::get((int) $link)) {
- $options .= '';
+ $options .= '';
}
}
diff --git a/src/RexVar/LinkVar.php b/src/RexVar/LinkVar.php
index b386585ad5..980f18eea8 100644
--- a/src/RexVar/LinkVar.php
+++ b/src/RexVar/LinkVar.php
@@ -22,12 +22,12 @@ public static function getWidget($id, $name, $value, array $args = [])
{
$artName = '';
$art = $value ? Article::get($value) : null;
- $category = Category::getCurrent() ? Category::getCurrent()->getId() : 0; // Aktuelle Kategorie vorauswählen
+ $category = Category::getCurrent()->id ?? 0; // Aktuelle Kategorie vorauswählen
// Falls ein Artikel vorausgewählt ist, dessen Namen anzeigen und beim Öffnen der Linkmap dessen Kategorie anzeigen
if ($art instanceof Article) {
- $artName = trim(sprintf('%s [%s]', $art->getName(), $art->getId()));
- $category = $art->getCategoryId();
+ $artName = trim(sprintf('%s [%s]', $art->name, $art->id));
+ $category = $art->categoryId ?? 0;
}
// Falls ein Kategorie-Parameter angegeben wurde, die Linkmap in dieser Kategorie öffnen
diff --git a/src/View/Navigation.php b/src/View/Navigation.php
index 3a7068d529..f6855fe163 100644
--- a/src/View/Navigation.php
+++ b/src/View/Navigation.php
@@ -167,8 +167,8 @@ public function getBreadcrumb($startPageLabel, $includeCurrent = false, $categor
}
}
- $cat = Category::get($pathItem);
- $link = $this->getBreadcrumbLinkTag($cat, escape($cat->getName()), [
+ $cat = Category::require($pathItem);
+ $link = $this->getBreadcrumbLinkTag($cat, escape($cat->name), [
'href' => $cat->getUrl(),
], $i);
$lis[] = $this->getBreadcrumbListItemTag($link, [
@@ -180,13 +180,13 @@ public function getBreadcrumb($startPageLabel, $includeCurrent = false, $categor
if ($includeCurrent) {
if ($art = Article::get($this->currentArticleId)) {
if (!$art->isStartArticle()) {
- $lis[] = $this->getBreadcrumbListItemTag(escape($art->getName()), [
+ $lis[] = $this->getBreadcrumbListItemTag(escape($art->name), [
'class' => 'rex-lvl' . $i,
], $i);
}
} else {
- $cat = Category::get($this->currentArticleId);
- $lis[] = $this->getBreadcrumbListItemTag(escape($cat->getName()), [
+ $cat = Category::require($this->currentArticleId);
+ $lis[] = $this->getBreadcrumbListItemTag(escape($cat->name), [
'class' => 'rex-lvl' . $i,
], $i);
}
@@ -265,15 +265,9 @@ private function _setActivePath()
{
$articleId = Article::getCurrentId();
if ($OOArt = Article::get($articleId)) {
- $path = trim($OOArt->getPath(), '|');
-
- $this->path = [];
- if ('' != $path) {
- $this->path = array_map(intval(...), explode('|', $path));
- }
-
+ $this->path = $OOArt->path;
$this->currentArticleId = $articleId;
- $this->currentCategoryId = $OOArt->getCategoryId();
+ $this->currentCategoryId = $OOArt->categoryId ?? 0;
return true;
}
@@ -394,14 +388,14 @@ protected function _getNavigation($categoryId, $depth = 1)
$li['class'] = [];
$a['class'] = [];
$a['href'] = [$nav->getUrl()];
- $aContent = escape($nav->getName());
+ $aContent = escape($nav->name);
if ($this->checkFilter($nav, $depth) && $this->checkCallbacks($nav, $depth, $li, $a, $aContent)) {
- $li['class'][] = 'rex-article-' . $nav->getId();
+ $li['class'][] = 'rex-article-' . $nav->id;
// classes abhaengig vom pfad
- if ($nav->getId() == $this->currentCategoryId) {
+ if ($nav->id == $this->currentCategoryId) {
$li['class'][] = 'rex-current';
$a['class'][] = 'rex-current';
- } elseif (in_array($nav->getId(), $this->path)) {
+ } elseif (in_array($nav->id, $this->path)) {
$li['class'][] = 'rex-active';
$a['class'][] = 'rex-active';
} else {
@@ -418,10 +412,10 @@ protected function _getNavigation($categoryId, $depth = 1)
++$depth;
if (
- ($this->open || $nav->getId() == $this->currentCategoryId || in_array($nav->getId(), $this->path))
+ ($this->open || $nav->id == $this->currentCategoryId || in_array($nav->id, $this->path))
&& ($this->depth >= $depth || $this->depth < 0)
) {
- $link .= "\n" . $this->_getNavigation($nav->getId(), $depth);
+ $link .= "\n" . $this->_getNavigation($nav->id, $depth);
}
--$depth;
$lis[] = $this->getListItemTag($nav, $link, $li, $depth);
diff --git a/src/View/View.php b/src/View/View.php
index 9a5eeebb6f..f84c32b0c0 100644
--- a/src/View/View.php
+++ b/src/View/View.php
@@ -6,6 +6,7 @@
use Redaxo\Core\Backend\Navigation;
use Redaxo\Core\Backend\Page;
use Redaxo\Core\Content\Article;
+use Redaxo\Core\Content\Category;
use Redaxo\Core\Core;
use Redaxo\Core\Exception\InvalidArgumentException;
use Redaxo\Core\ExtensionPoint\Extension;
@@ -247,11 +248,11 @@ public static function structureBreadcrumb(int $categoryId, int $articleId, int
$tree[] = $object;
}
foreach ($tree as $parent) {
- $id = $parent->getId();
+ $id = $parent->id;
if (Core::requireUser()->getComplexPerm('structure')->hasCategoryPerm($id)) {
$n = [];
- $n['title'] = str_replace(' ', ' ', escape($parent->getName()));
- if ($parent->isStartArticle()) {
+ $n['title'] = str_replace(' ', ' ', escape($parent->name));
+ if ($parent instanceof Category) {
$n['href'] = Url::backendPage('structure', ['category_id' => $id, 'clang' => $clang]);
}
$navigation[] = $n;
diff --git a/tests/Content/ArticleContentTest.php b/tests/Content/ArticleContentTest.php
index 198894800f..bfd113be20 100644
--- a/tests/Content/ArticleContentTest.php
+++ b/tests/Content/ArticleContentTest.php
@@ -13,7 +13,6 @@
use Redaxo\Core\Filesystem\File;
use Redaxo\Core\Filesystem\Finder;
use Redaxo\Core\Filesystem\Path;
-use ReflectionClass;
use ReflectionProperty;
/** @internal */
@@ -44,15 +43,6 @@ protected function setUp(): void
'art_foo' => 'teststring',
]);
-
- // generate classVars and add test column
- Article::getClassVars();
- $class = new ReflectionClass(Article::class);
- /** @psalm-suppress MixedArgument */
- $class->setStaticPropertyValue('classVars', array_merge(
- $class->getStaticPropertyValue('classVars'),
- ['art_foo'],
- ));
}
protected function tearDown(): void
@@ -64,10 +54,6 @@ protected function tearDown(): void
->ignoreSystemStuff(false);
Dir::deleteIterator($finder);
- // reset static properties
- $class = new ReflectionClass(Article::class);
- $class->setStaticPropertyValue('classVars', null);
-
Article::clearInstancePool();
}
diff --git a/tests/Content/ArticleTest.php b/tests/Content/ArticleTest.php
index 1ecd9462f5..4b63c6c947 100644
--- a/tests/Content/ArticleTest.php
+++ b/tests/Content/ArticleTest.php
@@ -2,40 +2,30 @@
namespace Redaxo\Core\Tests\Content;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Redaxo\Core\Content\Article;
+use Redaxo\Core\Content\Category;
+use Redaxo\Core\Content\StructureElement;
use ReflectionClass;
+use ReflectionProperty;
/** @internal */
final class ArticleTest extends TestCase
{
- protected function setUp(): void
- {
- // generate classVars and add test column
- Article::getClassVars();
- $class = new ReflectionClass(Article::class);
- /** @psalm-suppress MixedArgument */
- $class->setStaticPropertyValue('classVars', array_merge(
- $class->getStaticPropertyValue('classVars'),
- ['art_foo'],
- ));
- }
+ private static int $nextId = 20000;
- protected function tearDown(): void
+ public static function tearDownAfterClass(): void
{
- // reset static properties
- $class = new ReflectionClass(Article::class);
- $class->setStaticPropertyValue('classVars', null);
-
Article::clearInstancePool();
+ // Categories created here are left in the pool — clearing them would break
+ // CategoryTest, which already populated its own pool entries via data providers
+ // and runs after ArticleTest alphabetically.
}
public function testHasValue(): void
{
- $instance = $this->createArticleWithoutConstructor();
-
- /** @psalm-suppress UndefinedPropertyAssignment */
- $instance->art_foo = 'teststring'; // @phpstan-ignore-line
+ $instance = $this->createArticleWithAdditionalData(['art_foo' => 'teststring']);
self::assertTrue($instance->hasValue('foo'));
self::assertTrue($instance->hasValue('art_foo'));
@@ -46,10 +36,7 @@ public function testHasValue(): void
public function testGetValue(): void
{
- $instance = $this->createArticleWithoutConstructor();
-
- /** @psalm-suppress UndefinedPropertyAssignment */
- $instance->art_foo = 'teststring'; // @phpstan-ignore-line
+ $instance = $this->createArticleWithAdditionalData(['art_foo' => 'teststring']);
self::assertEquals('teststring', $instance->getValue('foo'));
self::assertEquals('teststring', $instance->getValue('art_foo'));
@@ -58,8 +45,193 @@ public function testGetValue(): void
self::assertNull($instance->getValue('art_bar'));
}
- private function createArticleWithoutConstructor(): Article
+ /** @param callable(StructureElement):bool $callback */
+ #[DataProvider('dataGetClosest')]
+ public function testGetClosest(?StructureElement $expected, Article $article, callable $callback): void
+ {
+ self::assertSame($expected, $article->getClosest($callback));
+ }
+
+ /** @return iterable */
+ public static function dataGetClosest(): iterable
+ {
+ $statusCallback = static fn (StructureElement $el): bool => 1 === $el->getValue('status');
+
+ // online article in online tree → article itself
+ [$_, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 1]);
+ $article = self::createArticle($lev3, ['status' => 1]);
+ yield [$article, $article, $statusCallback];
+
+ // offline article, online container category → category
+ [$_, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 1]);
+ $article = self::createArticle($lev3, ['status' => 0]);
+ yield [$lev3, $article, $statusCallback];
+
+ // offline article + offline container, online grandparent → grandparent
+ [$lev1, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 0], ['status' => 0]);
+ $article = self::createArticle($lev3, ['status' => 0]);
+ yield [$lev1, $article, $statusCallback];
+
+ // nothing matches → null
+ [$_, $_, $lev3] = self::createCategories(['status' => 0], ['status' => 0], ['status' => 0]);
+ $article = self::createArticle($lev3, ['status' => 0]);
+ yield [null, $article, $statusCallback];
+
+ // meta-info value found via metaInfoPrefix lookup: art_foo on article, cat_foo on cats
+ $fooCallback = static fn (StructureElement $el): bool => $el->getValue('foo') > 3;
+ [$lev1, $_, $lev3] = self::createCategories(['cat_foo' => 4], [], ['cat_foo' => 2]);
+ $article = self::createArticle($lev3, ['art_foo' => 1]);
+ yield [$lev1, $article, $fooCallback];
+
+ // article's own art_foo matches first
+ [$_, $_, $lev3] = self::createCategories(['cat_foo' => 4], [], ['cat_foo' => 2]);
+ $article = self::createArticle($lev3, ['art_foo' => 5]);
+ yield [$article, $article, $fooCallback];
+ }
+
+ #[DataProvider('dataGetClosestValue')]
+ public function testGetClosestValue(string|int|null $expectedValue, Article $article): void
+ {
+ self::assertSame($expectedValue, $article->getClosestValue('foo'));
+ }
+
+ /** @return iterable */
+ public static function dataGetClosestValue(): iterable
+ {
+ [$_, $_, $lev3] = self::createCategories([], [], []);
+ yield [null, self::createArticle($lev3, [])];
+
+ // article's own art_foo wins over everything else in the cat tree
+ [$_, $_, $lev3] = self::createCategories(['cat_foo' => 'baz'], ['cat_foo' => 'bar'], ['cat_foo' => 'foo']);
+ yield ['from-article', self::createArticle($lev3, ['art_foo' => 'from-article'])];
+
+ // no art_foo on article — falls through to cat_foo on containing cat
+ [$_, $_, $lev3] = self::createCategories([], [], ['cat_foo' => 'foo']);
+ yield ['foo', self::createArticle($lev3, [])];
+
+ // direct cat wins over grandparents
+ [$_, $_, $lev3] = self::createCategories([], ['cat_foo' => 'bar'], ['cat_foo' => 'foo']);
+ yield ['foo', self::createArticle($lev3, [])];
+
+ // walks up to nearest cat with value
+ [$_, $_, $lev3] = self::createCategories([], ['cat_foo' => 'bar'], []);
+ yield ['bar', self::createArticle($lev3, [])];
+
+ [$_, $_, $lev3] = self::createCategories(['cat_foo' => 'baz'], [], []);
+ yield ['baz', self::createArticle($lev3, [])];
+
+ [$_, $_, $lev3] = self::createCategories([], ['cat_foo' => 0], []);
+ yield [0, self::createArticle($lev3, [])];
+
+ // start-articles fall through to their parent category — own cat (lev3) is skipped
+ [$_, $_, $lev3] = self::createCategories(['cat_foo' => 'baz'], ['cat_foo' => 'bar'], ['cat_foo' => 'foo']);
+ yield ['bar', self::createArticle($lev3, [], startArticle: true)];
+ }
+
+ #[DataProvider('dataIsOnlineIncludingParents')]
+ public function testIsOnlineIncludingParents(bool $expected, Article $article): void
+ {
+ self::assertSame($expected, $article->isOnlineIncludingParents());
+ }
+
+ /** @return iterable */
+ public static function dataIsOnlineIncludingParents(): iterable
+ {
+ [$_, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 1]);
+ yield [true, self::createArticle($lev3, ['status' => 1])];
+
+ [$_, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 1]);
+ yield [false, self::createArticle($lev3, ['status' => 0])];
+
+ [$_, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 0]);
+ yield [false, self::createArticle($lev3, ['status' => 1])];
+
+ [$_, $_, $lev3] = self::createCategories(['status' => 0], ['status' => 1], ['status' => 1]);
+ yield [false, self::createArticle($lev3, ['status' => 1])];
+
+ // start-article in an offline parent category
+ [$_, $_, $lev3] = self::createCategories(['status' => 0], ['status' => 1], ['status' => 1]);
+ yield [false, self::createArticle($lev3, ['status' => 1], startArticle: true)];
+ }
+
+ /** @param array $additionalData */
+ private function createArticleWithAdditionalData(array $additionalData): Article
+ {
+ $reflectionClass = new ReflectionClass(Article::class);
+ $article = $reflectionClass->newInstanceWithoutConstructor();
+
+ $reflectionClass->getProperty('additionalData')->setValue($article, $additionalData);
+
+ return $article;
+ }
+
+ /**
+ * @param array $lev1Params
+ * @param array $lev2Params
+ * @param array $lev3Params
+ * @return array{Category, Category, Category}
+ */
+ private static function createCategories(array $lev1Params, array $lev2Params, array $lev3Params): array
+ {
+ $lev1 = self::createCategory(null, $lev1Params);
+ $lev2 = self::createCategory($lev1, $lev2Params);
+ $lev3 = self::createCategory($lev2, $lev3Params);
+
+ return [$lev1, $lev2, $lev3];
+ }
+
+ /** @param array $params */
+ private static function createCategory(?Category $parent, array $params): Category
+ {
+ $id = self::$nextId++;
+ $status = isset($params['status']) ? (int) $params['status'] : 1;
+ unset($params['status']);
+
+ $reflectionClass = new ReflectionClass(Category::class);
+ $category = $reflectionClass->newInstanceWithoutConstructor();
+
+ $reflectionClass->getProperty('id')->setValue($category, $id);
+ $reflectionClass->getProperty('parentId')->setValue($category, $parent?->id);
+ $reflectionClass->getProperty('clangId')->setValue($category, 1);
+ $reflectionClass->getProperty('status')->setValue($category, $status);
+ $reflectionClass->getProperty('path')->setValue($category, []);
+ $reflectionClass->getProperty('additionalData')->setValue($category, $params);
+
+ self::registerInstance(Category::class, $id, $category);
+
+ return $category;
+ }
+
+ /** @param array $params */
+ private static function createArticle(Category $category, array $params, bool $startArticle = false): Article
+ {
+ $id = self::$nextId++;
+ $status = isset($params['status']) ? (int) $params['status'] : 1;
+ unset($params['status']);
+
+ $reflectionClass = new ReflectionClass(Article::class);
+ $article = $reflectionClass->newInstanceWithoutConstructor();
+
+ $reflectionClass->getProperty('id')->setValue($article, $id);
+ $reflectionClass->getProperty('clangId')->setValue($article, 1);
+ $reflectionClass->getProperty('status')->setValue($article, $status);
+ $reflectionClass->getProperty('path')->setValue($article, []);
+ $reflectionClass->getProperty('categoryId')->setValue($article, $category->id);
+ $reflectionClass->getProperty('startArticle')->setValue($article, $startArticle);
+ $reflectionClass->getProperty('additionalData')->setValue($article, $params);
+
+ self::registerInstance(Article::class, $id, $article);
+
+ return $article;
+ }
+
+ /** @param class-string $class */
+ private static function registerInstance(string $class, int $id, StructureElement $instance): void
{
- return new ReflectionClass(Article::class)->newInstanceWithoutConstructor();
+ $instancesProperty = new ReflectionProperty(StructureElement::class, 'instances');
+ /** @var array> $instances */
+ $instances = $instancesProperty->getValue();
+ $instances[$class][$id . '###1'] = $instance;
+ $instancesProperty->setValue(null, $instances);
}
}
diff --git a/tests/Content/CategoryTest.php b/tests/Content/CategoryTest.php
index 7bd8fb0450..f20fe2fb3f 100644
--- a/tests/Content/CategoryTest.php
+++ b/tests/Content/CategoryTest.php
@@ -4,40 +4,24 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
-use Redaxo\Core\Content\Article;
use Redaxo\Core\Content\Category;
+use Redaxo\Core\Content\StructureElement;
use ReflectionClass;
+use ReflectionProperty;
/** @internal */
final class CategoryTest extends TestCase
{
- protected function setUp(): void
- {
- // generate classVars and add test column
- Category::getClassVars();
- $class = new ReflectionClass(Category::class);
- /** @psalm-suppress MixedArgument */
- $class->setStaticPropertyValue('classVars', array_merge(
- $class->getStaticPropertyValue('classVars'),
- ['cat_foo'],
- ));
- }
+ private static int $nextId = 10000;
- protected function tearDown(): void
+ public static function tearDownAfterClass(): void
{
- // reset static properties
- $class = new ReflectionClass(Article::class);
- $class->setStaticPropertyValue('classVars', null);
-
Category::clearInstancePool();
}
public function testHasValue(): void
{
- $instance = $this->createCategoryWithoutConstructor();
-
- /** @psalm-suppress UndefinedPropertyAssignment */
- $instance->cat_foo = 'teststring'; // @phpstan-ignore-line
+ $instance = $this->createCategoryWithAdditionalData(['cat_foo' => 'teststring']);
self::assertTrue($instance->hasValue('foo'));
self::assertTrue($instance->hasValue('cat_foo'));
@@ -48,10 +32,7 @@ public function testHasValue(): void
public function testGetValue(): void
{
- $instance = $this->createCategoryWithoutConstructor();
-
- /** @psalm-suppress UndefinedPropertyAssignment */
- $instance->cat_foo = 'teststring'; // @phpstan-ignore-line
+ $instance = $this->createCategoryWithAdditionalData(['cat_foo' => 'teststring']);
self::assertEquals('teststring', $instance->getValue('foo'));
self::assertEquals('teststring', $instance->getValue('cat_foo'));
@@ -129,64 +110,78 @@ public function testGetClosest(?Category $expected, Category $category, callable
/** @return iterable */
public static function dataGetClosest(): iterable
{
- $callback = static function (Category $category) {
- return 1 === $category->getValue('status');
- };
+ $statusCallback = static fn (Category $category): bool => 1 === $category->getValue('status');
[$lev1, $_, $lev3] = self::createCategories(['status' => 0], ['status' => 0], ['status' => 0]);
- yield [null, $lev1, $callback];
- yield [null, $lev3, $callback];
+ yield [null, $lev1, $statusCallback];
+ yield [null, $lev3, $statusCallback];
[$lev1, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 1]);
- yield [$lev1, $lev1, $callback];
- yield [$lev3, $lev3, $callback];
+ yield [$lev1, $lev1, $statusCallback];
+ yield [$lev3, $lev3, $statusCallback];
[$_, $lev2, $lev3] = self::createCategories(['status' => 1], ['status' => 1], ['status' => 0]);
- yield [$lev2, $lev3, $callback];
+ yield [$lev2, $lev3, $statusCallback];
[$lev1, $_, $lev3] = self::createCategories(['status' => 1], ['status' => 0], ['status' => 0]);
- yield [$lev1, $lev3, $callback];
+ yield [$lev1, $lev3, $statusCallback];
- $callback = static function (Category $category) {
- return $category->getValue('cat_foo') > 3;
- };
+ $fooCallback = static fn (Category $category): bool => $category->getValue('cat_foo') > 3;
[$lev1, $_, $lev3] = self::createCategories(['cat_foo' => 4], [], ['cat_foo' => 2]);
- yield [$lev1, $lev3, $callback];
+ yield [$lev1, $lev3, $fooCallback];
}
- private function createCategoryWithoutConstructor(): Category
+ /** @param array $additionalData */
+ private function createCategoryWithAdditionalData(array $additionalData): Category
{
- return new ReflectionClass(Category::class)->newInstanceWithoutConstructor();
+ $reflectionClass = new ReflectionClass(Category::class);
+ $category = $reflectionClass->newInstanceWithoutConstructor();
+
+ $reflectionClass->getProperty('additionalData')->setValue($category, $additionalData);
+
+ return $category;
}
- /** @return array{Category, Category, Category} */
+ /**
+ * @param array $lev1Params
+ * @param array $lev2Params
+ * @param array $lev3Params
+ * @return array{Category, Category, Category}
+ */
private static function createCategories(array $lev1Params, array $lev2Params, array $lev3Params): array
{
$lev1 = self::createCategory(null, $lev1Params);
- $lev2 = self::createCategory($lev1, $lev2Params);
- $lev3 = self::createCategory($lev2, $lev3Params);
+ $lev2 = self::createCategory($lev1->id, $lev2Params);
+ $lev3 = self::createCategory($lev2->id, $lev3Params);
return [$lev1, $lev2, $lev3];
}
- private static function createCategory(?Category $parent, array $params): Category
+ /** @param array $params */
+ private static function createCategory(?int $parentId, array $params): Category
{
- return new class($parent, $params) extends Category {
- public function __construct(
- private ?Category $parent,
- array $params,
- ) {
- foreach ($params as $key => $value) {
- $this->$key = $value;
- }
- }
-
- public function getParent(): ?Category
- {
- /** @var static|null */
- return $this->parent;
- }
- };
+ $id = self::$nextId++;
+ $status = isset($params['status']) ? (int) $params['status'] : 1;
+ unset($params['status']);
+
+ $reflectionClass = new ReflectionClass(Category::class);
+ $category = $reflectionClass->newInstanceWithoutConstructor();
+
+ $reflectionClass->getProperty('id')->setValue($category, $id);
+ $reflectionClass->getProperty('parentId')->setValue($category, $parentId);
+ $reflectionClass->getProperty('clangId')->setValue($category, 1);
+ $reflectionClass->getProperty('status')->setValue($category, $status);
+ $reflectionClass->getProperty('path')->setValue($category, []);
+ $reflectionClass->getProperty('additionalData')->setValue($category, $params);
+
+ // register in instance pool so Category::get($id, 1) returns this instance
+ $instancesProperty = new ReflectionProperty(StructureElement::class, 'instances');
+ /** @var array> $instances */
+ $instances = $instancesProperty->getValue();
+ $instances[Category::class][$id . '###1'] = $category;
+ $instancesProperty->setValue(null, $instances);
+
+ return $category;
}
}