diff --git a/.github/workflows/test-application.yaml b/.github/workflows/test-application.yaml index 6c16f9b08..6db92ac79 100644 --- a/.github/workflows/test-application.yaml +++ b/.github/workflows/test-application.yaml @@ -137,7 +137,7 @@ jobs: coverage: none - name: Remove not required tooling - run: composer remove php-cs-fixer/shim --dev --no-interaction --no-update + run: composer remove php-cs-fixer/shim "*phpstan*" --dev --no-interaction --no-update - name: Require elasticsearch dependency run: composer require --dev elasticsearch/elasticsearch:"${{ matrix.elasticsearch-package-constraint }}" --no-interaction --no-update diff --git a/Content/ArticleDataProvider.php b/Content/ArticleDataProvider.php index b52dd0602..8340d9ae9 100644 --- a/Content/ArticleDataProvider.php +++ b/Content/ArticleDataProvider.php @@ -137,6 +137,7 @@ protected function getConfigurationBuilder(): BuilderInterface ['column' => 'created', 'title' => 'sulu_admin.created'], ['column' => 'title.raw', 'title' => 'sulu_admin.title'], ['column' => 'author_full_name.raw', 'title' => 'sulu_admin.author'], + ['column' => 'last_modified_or_authored', 'title' => 'sulu_article.last_modified_or_authored'], ] ); diff --git a/Content/ArticleResourceItem.php b/Content/ArticleResourceItem.php index 5e73201bf..c533a2ab4 100644 --- a/Content/ArticleResourceItem.php +++ b/Content/ArticleResourceItem.php @@ -110,6 +110,14 @@ public function getPublished(): \DateTime return $this->article->getPublished(); } + /** + * Returns lastModified. + */ + public function getLastModified(): ?\DateTime + { + return $this->article->getLastModified(); + } + /** * Returns authored. */ diff --git a/DependencyInjection/SuluArticleExtension.php b/DependencyInjection/SuluArticleExtension.php index cf69ebbff..8670894a5 100644 --- a/DependencyInjection/SuluArticleExtension.php +++ b/DependencyInjection/SuluArticleExtension.php @@ -15,6 +15,7 @@ use Sulu\Bundle\ArticleBundle\Document\ArticlePageDocument; use Sulu\Bundle\ArticleBundle\Document\Form\ArticleDocumentType; use Sulu\Bundle\ArticleBundle\Document\Form\ArticlePageDocumentType; +use Sulu\Bundle\ArticleBundle\Document\LocalizedLastModifiedBehavior; use Sulu\Bundle\ArticleBundle\Document\Structure\ArticleBridge; use Sulu\Bundle\ArticleBundle\Document\Structure\ArticlePageBridge; use Sulu\Bundle\ArticleBundle\Exception\ArticlePageNotFoundException; @@ -188,7 +189,9 @@ public function prepend(ContainerBuilder $container) ], 'forms' => [ 'directories' => [ - __DIR__ . '/../Resources/config/forms', + \class_exists(LocalizedLastModifiedBehavior::class) + ? __DIR__ . '/../Resources/config/forms' + : __DIR__ . '/../Resources/config/forms_sulu_25_or_lower', ], ], 'resources' => [ diff --git a/Document/ArticleDocument.php b/Document/ArticleDocument.php index f558216e8..068091ec7 100644 --- a/Document/ArticleDocument.php +++ b/Document/ArticleDocument.php @@ -56,7 +56,8 @@ class ArticleDocument implements UuidBehavior, ChildrenBehavior, ArticleInterface, ShadowLocaleBehavior, - WebspaceBehavior + WebspaceBehavior, + LocalizedLastModifiedBehavior { public const RESOURCE_KEY = 'articles'; @@ -147,6 +148,11 @@ class ArticleDocument implements UuidBehavior, */ protected $changed; + /** + * @var \DateTime|null + */ + protected $lastModified; + /** * @var int */ @@ -427,6 +433,32 @@ public function getAuthored() return $this->authored; } + /** + * @return bool + */ + public function getLastModifiedEnabled() + { + return null !== $this->lastModified; + } + + /** + * @param \DateTime|null $lastModified + * + * @return void + */ + public function setLastModified($lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * @return \DateTime|null + */ + public function getLastModified() + { + return $this->lastModified; + } + public function setAuthored($authored) { $this->authored = $authored; diff --git a/Document/ArticleViewDocument.php b/Document/ArticleViewDocument.php index deb85c183..31e37830f 100644 --- a/Document/ArticleViewDocument.php +++ b/Document/ArticleViewDocument.php @@ -168,6 +168,13 @@ class ArticleViewDocument implements ArticleViewDocumentInterface */ protected $seo; + /** + * @var \DateTime|null + * + * @Property(type="date") + */ + protected $lastModified; + /** * @var \DateTime * @@ -175,6 +182,13 @@ class ArticleViewDocument implements ArticleViewDocumentInterface */ protected $authored; + /** + * @var \DateTime + * + * @Property(type="date") + */ + protected $lastModifiedOrAuthored; + /** * @var string * @@ -484,6 +498,43 @@ public function setSeo(SeoViewObject $seo) return $this; } + public function getLastModified() + { + return $this->lastModified; + } + + public function setLastModified($lastModified) + { + $this->lastModified = $lastModified; + $this->updateLastModifiedOrAuthored(); + + return $this; + } + + public function getLastModifiedOrAuthored(): \DateTime + { + return $this->lastModified ?? $this->authored; + } + + /** + * @internal setter is required for ONGR but should not be used + * + * @return static + */ + public function setLastModifiedOrAuthored(\DateTime $lastModifiedOrAuthored) + { + $this->lastModifiedOrAuthored = $lastModifiedOrAuthored; + + return $this; + } + + public function updateLastModifiedOrAuthored() + { + $this->lastModifiedOrAuthored = $this->lastModified ?? $this->authored; + + return $this; + } + public function getAuthored() { return $this->authored; @@ -492,6 +543,7 @@ public function getAuthored() public function setAuthored(?\DateTime $authored = null) { $this->authored = $authored; + $this->updateLastModifiedOrAuthored(); return $this; } diff --git a/Document/ArticleViewDocumentInterface.php b/Document/ArticleViewDocumentInterface.php index ef28f1053..21df42a9b 100644 --- a/Document/ArticleViewDocumentInterface.php +++ b/Document/ArticleViewDocumentInterface.php @@ -257,6 +257,22 @@ public function getSeo(); */ public function setSeo(SeoViewObject $seo); + /** + * Returns lastModified. + * + * @return \DateTime|null + */ + public function getLastModified(); + + /** + * Set lastModified date. + * + * @param \DateTime|null $lastModified + * + * @return $this + */ + public function setLastModified($lastModified); + /** * Returns authored. * @@ -271,6 +287,18 @@ public function getAuthored(); */ public function setAuthored(?\DateTime $authored = null); + /** + * Returns lastModified or authored date. + * + * @return \DateTime|null + */ + public function getLastModifiedOrAuthored(); + + /** + * @return $this + */ + public function updateLastModifiedOrAuthored(); + /** * Returns author full name. * diff --git a/Document/Form/ArticleDocumentType.php b/Document/Form/ArticleDocumentType.php index 56cea89ae..4e68a8bab 100644 --- a/Document/Form/ArticleDocumentType.php +++ b/Document/Form/ArticleDocumentType.php @@ -46,6 +46,13 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); + $builder->add( + 'lastModified', + DateTimeType::class, + [ + 'widget' => 'single_text', + ] + ); $builder->add('author', TextType::class); $builder->add( 'authored', diff --git a/Document/Index/ArticleIndexer.php b/Document/Index/ArticleIndexer.php index f17e72ec7..4bcf3879c 100644 --- a/Document/Index/ArticleIndexer.php +++ b/Document/Index/ArticleIndexer.php @@ -187,6 +187,7 @@ protected function createOrUpdateArticle( $this->setParentPageUuid($document, $article); $article->setChanged($document->getChanged()); $article->setCreated($document->getCreated()); + $article->setLastModified($document->getLastModified()); $article->setAuthored($document->getAuthored()); if ($document->getAuthor() && $author = $this->contactRepository->find($document->getAuthor())) { $article->setAuthorId($author->getId()); diff --git a/Document/LocalizedLastModifiedBehavior.php b/Document/LocalizedLastModifiedBehavior.php new file mode 100644 index 000000000..fc55cfb7d --- /dev/null +++ b/Document/LocalizedLastModifiedBehavior.php @@ -0,0 +1,30 @@ +sulu_page.editing_information + + + sulu_article.last_modified_enabled + + + + + + + + + + sulu_article.last_modified_date + + sulu_page.authored_date diff --git a/Resources/config/forms_sulu_25_or_lower/article_settings.xml b/Resources/config/forms_sulu_25_or_lower/article_settings.xml new file mode 100644 index 000000000..f425626c9 --- /dev/null +++ b/Resources/config/forms_sulu_25_or_lower/article_settings.xml @@ -0,0 +1,128 @@ + +
+ article_settings + + + + + + + + + + + + + + + + + + + + + + + + + sulu_article.webspace_settings + + + + + + sulu_article.customize_webspace_settings + + + + + + + + + sulu_article.main_webspace + + + + + + + + + + sulu_article.additional_webspace + + + + + + + + +
+ + sulu_article.shadow_article + + + + + sulu_article.shadow_article + sulu_page.enable_shadow_page_info_text + + + + + + sulu_article.enable_shadow_article + + + + + + + sulu_page.shadow_locale + + + +
+ +
+ + sulu_page.editing_information + + + + + sulu_page.authored_date + + + + + sulu_page.author + + + + + sulu_page.changelog + + + +
+
+
diff --git a/Resources/config/serializer/Document.ArticleDocument.xml b/Resources/config/serializer/Document.ArticleDocument.xml index 9bee90b7d..5ceabb96a 100644 --- a/Resources/config/serializer/Document.ArticleDocument.xml +++ b/Resources/config/serializer/Document.ArticleDocument.xml @@ -25,6 +25,8 @@ + + diff --git a/Resources/config/serializer/Document.ArticleViewDocument.xml b/Resources/config/serializer/Document.ArticleViewDocument.xml index dc14198cd..a9fb4f9a3 100644 --- a/Resources/config/serializer/Document.ArticleViewDocument.xml +++ b/Resources/config/serializer/Document.ArticleViewDocument.xml @@ -9,6 +9,7 @@ + diff --git a/Resources/translations/admin.de.json b/Resources/translations/admin.de.json index 932a5e724..cf3ac8e3c 100644 --- a/Resources/translations/admin.de.json +++ b/Resources/translations/admin.de.json @@ -15,6 +15,9 @@ "sulu_article.published": "Veröffentlicht", "sulu_article.not_published": "Nicht Veröffentlicht", "sulu_article.route": "Route", + "sulu_article.last_modified_or_authored": "Zuletzt geändert oder verfasst", + "sulu_article.last_modified_enabled": "Aktualisierungsdatum aktivieren", + "sulu_article.last_modified_date": "Datum der Aktualisierung", "sulu_activity.resource.articles": "Artikel", "sulu_activity.resource.articles.translation": "Übersetzung", "sulu_activity.description.articles.created": "{userFullName} hat den Artikel \"{resourceTitle}\" erstellt", diff --git a/Resources/translations/admin.en.json b/Resources/translations/admin.en.json index 560ffa387..398a3adf7 100644 --- a/Resources/translations/admin.en.json +++ b/Resources/translations/admin.en.json @@ -15,6 +15,8 @@ "sulu_article.published": "Published", "sulu_article.not_published": "Not Published", "sulu_article.route": "Route", + "sulu_article.last_modified_enabled": "Enable last modified date", + "sulu_article.last_modified_date": "Last modified date", "sulu_activity.resource.articles": "Article", "sulu_activity.resource.articles.translation": "Translation", "sulu_activity.description.articles.created": "{userFullName} has created the article \"{resourceTitle}\"", diff --git a/Sitemap/ArticleSitemapProvider.php b/Sitemap/ArticleSitemapProvider.php index 2e5adc2ec..ee60a7e94 100644 --- a/Sitemap/ArticleSitemapProvider.php +++ b/Sitemap/ArticleSitemapProvider.php @@ -110,7 +110,7 @@ protected function buildUrl( $this->findUrl($articleView, $scheme, $host, $webspaceKey), $articleView->getLocale(), null, - $articleView->getChanged() + $articleView->getLastModified() ?? $articleView->getChanged() ); } diff --git a/Tests/Application/Kernel.php b/Tests/Application/Kernel.php index 56276f256..2b30fda4f 100644 --- a/Tests/Application/Kernel.php +++ b/Tests/Application/Kernel.php @@ -66,6 +66,9 @@ public function process(ContainerBuilder $container) ->setPublic(true); } + /** + * @return array + */ protected function getKernelParameters(): array { $parameters = parent::getKernelParameters(); diff --git a/Trash/ArticleTrashItemHandler.php b/Trash/ArticleTrashItemHandler.php index 7d9fdbd92..23a9bf095 100644 --- a/Trash/ArticleTrashItemHandler.php +++ b/Trash/ArticleTrashItemHandler.php @@ -98,12 +98,14 @@ public function store(object $article, array $options = []): TrashItemInterface $structureData = $localizedArticle->getStructure()->toArray(); $articleTitles[$locale] = $localizedArticle->getTitle(); + $lastModified = $localizedArticle->getLastModified() ? $localizedArticle->getLastModified()->format('c') : null; $data['locales'][] = [ 'title' => $localizedArticle->getTitle(), 'locale' => $locale, 'creator' => $localizedArticle->getCreator(), 'created' => $localizedArticle->getCreated()->format('c'), + 'lastModified' => $lastModified, 'author' => $localizedArticle->getAuthor(), 'authored' => $localizedArticle->getAuthored()->format('c'), 'structureType' => $localizedArticle->getStructureType(), @@ -145,6 +147,23 @@ public function restore(TrashItemInterface $trashItem, array $restoreFormData = } } + /** @var array{ + * title: string, + * locale: string, + * creator: ?int, + * created: string, + * lastModified: ?string, + * author: ?int, + * authored: string, + * structureType: string, + * structureData: mixed, + * extensionsData: mixed, + * shadowLocaleEnabled: bool, + * shadowLocale: ?string, + * mainWebspace: ?string, + * additionalWebspaces: ?array, + * } $localeData + */ foreach ($sortedLocales as $localeData) { $locale = $localeData['locale']; @@ -157,11 +176,13 @@ public function restore(TrashItemInterface $trashItem, array $restoreFormData = $localizedArticle->setParent($this->documentManager->find($data['parentUuid'])); $localizedArticle->setUuid($uuid); } + $lastModified = \array_key_exists('lastModified', $localeData) && $localeData['lastModified'] ? new \DateTime($localeData['lastModified']) : null; $localizedArticle->setTitle($localeData['title']); $localizedArticle->setLocale($locale); $localizedArticle->setCreator($localeData['creator']); $localizedArticle->setCreated(new \DateTime($localeData['created'])); + $localizedArticle->setLastModified($lastModified); $localizedArticle->setAuthor($localeData['author']); $localizedArticle->setAuthored(new \DateTime($localeData['authored'])); $localizedArticle->setStructureType($localeData['structureType']); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 76eaf1d6a..515a4424b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2430,78 +2430,8 @@ parameters: count: 1 path: Trash/ArticleTrashItemHandler.php - - - message: "#^Cannot access offset 'additionalWebspaces' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'author' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'authored' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'created' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'creator' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'extensionsData' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'locale' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'mainWebspace' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'shadowLocale' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - message: "#^Cannot access offset 'shadowLocaleEnabled' on mixed\\.$#" - count: 2 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'structureData' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'structureType' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Cannot access offset 'title' on mixed\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$additionalWebspaces of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setAdditionalWebspaces\\(\\) expects array\\\\|null, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$author of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setAuthor\\(\\) expects int\\|null, mixed given\\.$#" count: 1 path: Trash/ArticleTrashItemHandler.php @@ -2510,66 +2440,16 @@ parameters: count: 1 path: Trash/ArticleTrashItemHandler.php - - - message: "#^Parameter \\#1 \\$datetime of class DateTime constructor expects string, mixed given\\.$#" - count: 2 - path: Trash/ArticleTrashItemHandler.php - - message: "#^Parameter \\#1 \\$extensions of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setExtensionsData\\(\\) expects array\\\\|Sulu\\\\Component\\\\Content\\\\Document\\\\Extension\\\\ExtensionContainer, mixed given\\.$#" count: 1 path: Trash/ArticleTrashItemHandler.php - - - message: "#^Parameter \\#1 \\$locale of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setLocale\\(\\) expects string, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$mainWebspace of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setMainWebspace\\(\\) expects string\\|null, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$shadowLocale of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setShadowLocale\\(\\) expects string\\|null, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$shadowLocaleEnabled of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setShadowLocaleEnabled\\(\\) expects bool, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$structureType of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setStructureType\\(\\) expects string, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$title of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setTitle\\(\\) expects string, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#1 \\$userId of method Sulu\\\\Bundle\\\\ArticleBundle\\\\Document\\\\ArticleDocument\\:\\:setCreator\\(\\) expects int\\|null, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - message: "#^Parameter \\#2 \\$locale of class Sulu\\\\Bundle\\\\ArticleBundle\\\\Domain\\\\Event\\\\ArticleTranslationRestoredEvent constructor expects string, mixed given\\.$#" count: 1 path: Trash/ArticleTrashItemHandler.php - - - message: "#^Parameter \\#2 \\$locale of method Sulu\\\\Component\\\\DocumentManager\\\\DocumentManagerInterface\\:\\:find\\(\\) expects string\\|null, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - - - message: "#^Parameter \\#2 \\$locale of method Sulu\\\\Component\\\\DocumentManager\\\\DocumentManagerInterface\\:\\:persist\\(\\) expects string\\|null, mixed given\\.$#" - count: 1 - path: Trash/ArticleTrashItemHandler.php - - message: "#^Cannot call method get\\(\\) on Symfony\\\\Component\\\\HttpFoundation\\\\Request\\|null\\.$#" count: 1