diff --git a/Content/ArticleSelectionContentType.php b/Content/ArticleSelectionContentType.php index 1f6a0ceae..c9461c766 100644 --- a/Content/ArticleSelectionContentType.php +++ b/Content/ArticleSelectionContentType.php @@ -18,7 +18,7 @@ use Sulu\Component\Content\SimpleContentType; /** - * TODO add description here. + * Provides article_selection content-type. */ class ArticleSelectionContentType extends SimpleContentType { diff --git a/Controller/ArticlePageController.php b/Controller/ArticlePageController.php new file mode 100644 index 000000000..98ea94ca4 --- /dev/null +++ b/Controller/ArticlePageController.php @@ -0,0 +1,239 @@ +getRequestParameter($request, 'locale', true); + $document = $this->getDocumentManager()->find( + $uuid, + $locale, + [ + 'load_ghost_content' => true, + 'load_shadow_content' => false, + ] + ); + + return $this->handleView( + $this->view($document)->setSerializationContext( + SerializationContext::create()->setSerializeNull(true)->setGroups(['defaultPage']) + ) + ); + } + + /** + * Create article-page. + * + * @param string $articleUuid + * @param Request $request + * + * @return Response + */ + public function postAction($articleUuid, Request $request) + { + $action = $request->get('action'); + $document = $this->getDocumentManager()->create(self::DOCUMENT_TYPE); + $locale = $this->getRequestParameter($request, 'locale', true); + $data = $request->request->all(); + + $this->persistDocument($data, $document, $locale, $articleUuid); + $this->handleActionParameter($action, $document, $locale); + $this->getDocumentManager()->flush(); + + return $this->handleView( + $this->view($document)->setSerializationContext( + SerializationContext::create()->setSerializeNull(true)->setGroups(['defaultPage']) + ) + ); + } + + /** + * Update article-page. + * + * @param string $articleUuid + * @param string $uuid + * @param Request $request + * + * @return Response + */ + public function putAction($articleUuid, $uuid, Request $request) + { + $locale = $this->getRequestParameter($request, 'locale', true); + $action = $request->get('action'); + $data = $request->request->all(); + + $document = $this->getDocumentManager()->find( + $uuid, + $locale, + [ + 'load_ghost_content' => false, + 'load_shadow_content' => false, + ] + ); + + $this->get('sulu_hash.request_hash_checker')->checkHash($request, $document, $document->getUuid()); + + $this->persistDocument($data, $document, $locale, $articleUuid); + $this->handleActionParameter($action, $document, $locale); + $this->getDocumentManager()->flush(); + + return $this->handleView( + $this->view($document)->setSerializationContext( + SerializationContext::create()->setSerializeNull(true)->setGroups(['defaultPage']) + ) + ); + } + + /** + * Delete article-page. + * + * @param string $articleUuid + * @param string $uuid + * + * @return Response + */ + public function deleteAction($articleUuid, $uuid) + { + $documentManager = $this->getDocumentManager(); + $document = $documentManager->find($uuid); + $documentManager->remove($document); + $documentManager->flush(); + + return $this->handleView($this->view(null)); + } + + /** + * {@inheritdoc} + */ + public function getSecurityContext() + { + return ArticleAdmin::SECURITY_CONTEXT; + } + + /** + * Persists the document using the given information. + * + * @param array $data + * @param object $document + * @param string $locale + * @param string $articleUuid + * + * @throws InvalidFormException + * @throws MissingParameterException + * @throws ParameterNotAllowedException + */ + private function persistDocument($data, $document, $locale, $articleUuid) + { + if (array_key_exists('title', $data)) { + throw new ParameterNotAllowedException('title', ArticlePageDocument::class); + } + if (array_key_exists('template', $data)) { + throw new ParameterNotAllowedException('template', ArticlePageDocument::class); + } + + $article = $this->getDocumentManager()->find($articleUuid, $locale); + $data['template'] = $article->getStructureType(); + + $form = $this->createForm( + ArticlePageDocumentType::class, + $document, + [ + // disable csrf protection, since we can't produce a token, because the form is cached on the client + 'csrf_protection' => false, + ] + ); + $form->submit($data, false); + + $document->setParent($article); + + if (!$form->isValid()) { + throw new InvalidFormException($form); + } + + $this->getDocumentManager()->persist( + $document, + $locale, + [ + 'user' => $this->getUser()->getId(), + 'clear_missing_content' => false, + ] + ); + } + + /** + * Returns document-manager. + * + * @return DocumentManagerInterface + */ + protected function getDocumentManager() + { + return $this->get('sulu_document_manager.document_manager'); + } + + /** + * @return ContentMapperInterface + */ + protected function getMapper() + { + return $this->get('sulu.content.mapper'); + } + + /** + * Delegates actions by given actionParameter, which can be retrieved from the request. + * + * @param string $actionParameter + * @param object $document + * @param string $locale + */ + private function handleActionParameter($actionParameter, $document, $locale) + { + switch ($actionParameter) { + case 'publish': + $this->getDocumentManager()->publish($document, $locale); + break; + } + } +} diff --git a/DependencyInjection/SuluArticleExtension.php b/DependencyInjection/SuluArticleExtension.php index 05bdda39e..998270ca0 100644 --- a/DependencyInjection/SuluArticleExtension.php +++ b/DependencyInjection/SuluArticleExtension.php @@ -12,7 +12,10 @@ namespace Sulu\Bundle\ArticleBundle\DependencyInjection; use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; +use Sulu\Bundle\ArticleBundle\Document\ArticlePageDocument; use Sulu\Bundle\ArticleBundle\Document\Structure\ArticleBridge; +use Sulu\Bundle\ArticleBundle\Document\Structure\ArticlePageBridge; +use Sulu\Bundle\ArticleBundle\Exception\ParameterNotAllowedException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -37,6 +40,7 @@ public function prepend(ContainerBuilder $container) 'structure' => [ 'type_map' => [ 'article' => ArticleBridge::class, + 'article_page' => ArticlePageBridge::class, ], ], ], @@ -67,6 +71,7 @@ public function prepend(ContainerBuilder $container) 'search' => [ 'mapping' => [ ArticleDocument::class => ['index' => 'article'], + ArticlePageDocument::class => ['index' => 'article_page'], ], ], ] @@ -79,6 +84,7 @@ public function prepend(ContainerBuilder $container) [ 'mapping' => [ 'article' => ['class' => ArticleDocument::class, 'phpcr_type' => 'sulu:article'], + 'article_page' => ['class' => ArticlePageDocument::class, 'phpcr_type' => 'sulu:articlepage'], ], 'path_segments' => [ 'articles' => 'articles', @@ -86,6 +92,19 @@ public function prepend(ContainerBuilder $container) ] ); } + + if ($container->hasExtension('fos_rest')) { + $container->prependExtensionConfig( + 'fos_rest', + [ + 'exception' => [ + 'codes' => [ + ParameterNotAllowedException::class => 400, + ], + ], + ] + ); + } } /** diff --git a/Document/ArticleDocument.php b/Document/ArticleDocument.php index e69493dda..1b85eadab 100644 --- a/Document/ArticleDocument.php +++ b/Document/ArticleDocument.php @@ -23,12 +23,14 @@ use Sulu\Component\Content\Document\Extension\ExtensionContainer; use Sulu\Component\Content\Document\Structure\Structure; use Sulu\Component\Content\Document\Structure\StructureInterface; +use Sulu\Component\DocumentManager\Behavior\Mapping\ChildrenBehavior; use Sulu\Component\DocumentManager\Behavior\Mapping\LocalizedTitleBehavior; use Sulu\Component\DocumentManager\Behavior\Mapping\NodeNameBehavior; use Sulu\Component\DocumentManager\Behavior\Mapping\PathBehavior; use Sulu\Component\DocumentManager\Behavior\Mapping\UuidBehavior; use Sulu\Component\DocumentManager\Behavior\Path\AutoNameBehavior; use Sulu\Component\DocumentManager\Behavior\VersionBehavior; +use Sulu\Component\DocumentManager\Collection\ChildrenCollection; use Sulu\Component\DocumentManager\Version; /** @@ -48,7 +50,8 @@ class ArticleDocument implements ExtensionBehavior, WorkflowStageBehavior, VersionBehavior, - AuthorBehavior + AuthorBehavior, + ChildrenBehavior { /** * @var string @@ -163,10 +166,16 @@ class ArticleDocument implements */ protected $versions = []; + /** + * @var ChildrenCollection + */ + protected $children; + public function __construct() { $this->structure = new Structure(); $this->extensions = new ExtensionContainer(); + $this->children = new \ArrayIterator(); } /** @@ -463,4 +472,12 @@ public function setVersions($versions) { $this->versions = $versions; } + + /** + * {@inheritdoc} + */ + public function getChildren() + { + return $this->children; + } } diff --git a/Document/ArticlePageDocument.php b/Document/ArticlePageDocument.php new file mode 100644 index 000000000..9abe66803 --- /dev/null +++ b/Document/ArticlePageDocument.php @@ -0,0 +1,217 @@ +structure = new Structure(); + } + + /** + * {@inheritdoc} + */ + public function getUuid() + { + return $this->uuid; + } + + /** + * {@inheritdoc} + */ + public function setUuid($uuid) + { + $this->uuid = $uuid; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTitle() + { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return $this->parent; + } + + /** + * {@inheritdoc} + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + return $this->path; + } + + /** + * {@inheritdoc} + */ + public function getLocale() + { + return $this->locale; + } + + /** + * {@inheritdoc} + */ + public function setLocale($locale) + { + $this->locale = $locale; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOriginalLocale() + { + return $this->originalLocale; + } + + /** + * {@inheritdoc} + */ + public function setOriginalLocale($originalLocale) + { + $this->originalLocale = $originalLocale; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getStructureType() + { + return $this->structureType; + } + + /** + * {@inheritdoc} + */ + public function setStructureType($structureType) + { + $this->structureType = $structureType; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getStructure() + { + return $this->structure; + } + + /** + * Returns page. + * + * @return int + */ + public function getPageNumber() + { + return $this->pageNumber; + } +} diff --git a/Document/ArticlePageViewObject.php b/Document/ArticlePageViewObject.php new file mode 100644 index 000000000..e945b4e7f --- /dev/null +++ b/Document/ArticlePageViewObject.php @@ -0,0 +1,52 @@ +uuid = $uuid; } @@ -682,4 +689,22 @@ public function getChangerContactId() { return $this->changerContactId; } + + /** + * {@inheritdoc} + */ + public function getPages() + { + return $this->pages; + } + + /** + * {@inheritdoc} + */ + public function setPages(Collection $pages) + { + $this->pages = $pages; + + return $this; + } } diff --git a/Document/ArticleViewDocumentInterface.php b/Document/ArticleViewDocumentInterface.php index 4ad2d237d..3383195de 100644 --- a/Document/ArticleViewDocumentInterface.php +++ b/Document/ArticleViewDocumentInterface.php @@ -11,6 +11,8 @@ namespace Sulu\Bundle\ArticleBundle\Document; +use ONGR\ElasticsearchBundle\Collection\Collection; + /** * Interface for indexable article-document. */ @@ -399,4 +401,20 @@ public function setChangerContactId($changerContactId); * @return string */ public function getChangerContactId(); + + /** + * Returns pages. + * + * @return ArticlePageViewObject[] + */ + public function getPages(); + + /** + * Set pages. + * + * @param Collection $pages + * + * @return $this + */ + public function setPages(Collection $pages); } diff --git a/Document/Form/ArticleDocumentType.php b/Document/Form/ArticleDocumentType.php index 862e1b9fe..db0266db1 100644 --- a/Document/Form/ArticleDocumentType.php +++ b/Document/Form/ArticleDocumentType.php @@ -32,17 +32,12 @@ public function buildForm(FormBuilderInterface $builder, array $options) // extensions $builder->add('extensions', TextType::class, ['property_path' => 'extensionsData']); - // TODO: Fix the admin interface to not send this junk (not required for articles) - $builder->add('redirectType', TextType::class, ['mapped' => false]); - $builder->add('resourceSegment', TextType::class, ['mapped' => false]); - $builder->add('navigationContexts', TextType::class, ['mapped' => false]); - $builder->add('shadowLocaleEnabled', TextType::class, ['mapped' => false]); + $builder->add('author', TextType::class); $builder->add( 'authored', DateType::class, ['widget' => 'single_text', 'model_timezone' => 'UTC', 'view_timezone' => 'UTC'] ); - $builder->add('author', TextType::class); } /** diff --git a/Document/Form/ArticlePageDocumentType.php b/Document/Form/ArticlePageDocumentType.php new file mode 100644 index 000000000..d85e029e3 --- /dev/null +++ b/Document/Form/ArticlePageDocumentType.php @@ -0,0 +1,33 @@ +setDefaults( + [ + 'allow_extra_fields' => true, + ] + ); + } +} diff --git a/Document/Index/ArticleIndexer.php b/Document/Index/ArticleIndexer.php index 71335fa4f..1bc99f204 100644 --- a/Document/Index/ArticleIndexer.php +++ b/Document/Index/ArticleIndexer.php @@ -11,10 +11,11 @@ namespace Sulu\Bundle\ArticleBundle\Document\Index; +use ONGR\ElasticsearchBundle\Collection\Collection; use ONGR\ElasticsearchBundle\Service\Manager; use ONGR\ElasticsearchDSL\Query\MatchAllQuery; use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; -use Sulu\Bundle\ArticleBundle\Document\ArticleViewDocument; +use Sulu\Bundle\ArticleBundle\Document\ArticlePageViewObject; use Sulu\Bundle\ArticleBundle\Document\ArticleViewDocumentInterface; use Sulu\Bundle\ArticleBundle\Document\Index\Factory\ExcerptFactory; use Sulu\Bundle\ArticleBundle\Document\Index\Factory\SeoFactory; @@ -151,9 +152,9 @@ private function getTypeTranslation($type) /** * @param ArticleDocument $document - * @param ArticleViewDocument $article + * @param ArticleViewDocumentInterface $article */ - protected function dispatchIndexEvent(ArticleDocument $document, ArticleViewDocument $article) + protected function dispatchIndexEvent(ArticleDocument $document, ArticleViewDocumentInterface $article) { $this->eventDispatcher->dispatch(Events::INDEX_EVENT, new IndexEvent($document, $article)); } @@ -170,22 +171,9 @@ protected function createOrUpdateArticle( $locale, $localizationState = LocalizationState::LOCALIZED ) { - $articleId = $this->getViewDocumentId($document->getUuid(), $locale); - /** @var ArticleViewDocument $article */ - $article = $this->manager->find($this->documentFactory->getClass('article'), $articleId); - + $article = $this->findOrCreateViewDocument($document, $locale, $localizationState); if (!$article) { - $article = $this->documentFactory->create('article'); - $article->setId($articleId); - $article->setUuid($document->getUuid()); - $article->setLocale($locale); - } else { - // Only index ghosts when the article isn't a ghost himself. - if (LocalizationState::GHOST === $localizationState - && LocalizationState::GHOST !== $article->getLocalizationState()->state - ) { - return null; - } + return; } $structureMetadata = $this->structureMetadataFactory->getStructureMetadata( @@ -243,11 +231,66 @@ protected function createOrUpdateArticle( } } + $this->mapPages($document, $article); + $this->manager->persist($article); return $article; } + /** + * Returns view-document from index or create a new one. + * + * @param ArticleDocument $document + * @param string $locale + * @param string $localizationState + * + * @return ArticleViewDocumentInterface + */ + protected function findOrCreateViewDocument(ArticleDocument $document, $locale, $localizationState) + { + $articleId = $this->getViewDocumentId($document->getUuid(), $locale); + /** @var ArticleViewDocumentInterface $article */ + $article = $this->manager->find($this->documentFactory->getClass('article'), $articleId); + + if ($article) { + // Only index ghosts when the article isn't a ghost himself. + if (LocalizationState::GHOST === $localizationState + && LocalizationState::GHOST !== $article->getLocalizationState()->state + ) { + return null; + } + + return $article; + } + + $article = $this->documentFactory->create('article'); + $article->setId($articleId); + $article->setUuid($document->getUuid()); + $article->setLocale($locale); + + return $article; + } + + /** + * Maps pages from document to view-document. + * + * @param ArticleDocument $document + * @param ArticleViewDocumentInterface $article + */ + private function mapPages(ArticleDocument $document, ArticleViewDocumentInterface $article) + { + $pages = []; + foreach ($document->getChildren() as $child) { + $pages[] = $page = new ArticlePageViewObject(); + $page->uuid = $child->getUuid(); + $page->pageNumber = $child->getPageNumber(); + $page->title = $child->getTitle(); + } + + $article->setPages(new Collection($pages)); + } + /** * @param string $id */ diff --git a/Document/Initializer/ArticleInitializer.php b/Document/Initializer/ArticleInitializer.php index e846e0dd8..2f463dee1 100644 --- a/Document/Initializer/ArticleInitializer.php +++ b/Document/Initializer/ArticleInitializer.php @@ -54,6 +54,7 @@ public function initialize(OutputInterface $output, $purge = false) { $nodeTypeManager = $this->sessionManager->getSession()->getWorkspace()->getNodeTypeManager(); $nodeTypeManager->registerNodeType(new ArticleNodeType(), true); + $nodeTypeManager->registerNodeType(new ArticlePageNodeType(), true); $articlesPath = $this->pathBuilder->build(['%base%', '%articles%']); if (true === $this->nodeManager->has($articlesPath)) { diff --git a/Document/Initializer/ArticlePageNodeType.php b/Document/Initializer/ArticlePageNodeType.php new file mode 100644 index 000000000..428a97130 --- /dev/null +++ b/Document/Initializer/ArticlePageNodeType.php @@ -0,0 +1,94 @@ + Events::POST_SERIALIZE, + 'format' => 'json', + 'method' => 'addTitleOnPostSerialize', + ], + ]; + } + + /** + * Append title to result. + * + * @param ObjectEvent $event + */ + public function addTitleOnPostSerialize(ObjectEvent $event) + { + $articlePage = $event->getObject(); + $visitor = $event->getVisitor(); + $context = $event->getContext(); + + if (!$articlePage instanceof ArticlePageDocument) { + return; + } + + $visitor->addData('title', $context->accept($articlePage->getParent()->getTitle())); + } +} diff --git a/Document/Structure/ArticlePageBridge.php b/Document/Structure/ArticlePageBridge.php new file mode 100644 index 000000000..44e111811 --- /dev/null +++ b/Document/Structure/ArticlePageBridge.php @@ -0,0 +1,28 @@ +structureMetadataFactory = $structureMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + Events::PERSIST => [['setTitleOnPersist', 2048], ['setPageNumberOnPersist', 0]], + Events::HYDRATE => [['setPageNumberOnHydrate', 0]], + ]; + } + + /** + * Set page-title from structure to document. + * + * @param PersistEvent $event + */ + public function setTitleOnPersist(PersistEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof ArticlePageDocument) { + return; + } + + $pageTitle = uniqid('page-', true); + $pageTitleProperty = $this->getPageTitleProperty($document); + + if ($pageTitleProperty) { + $pageTitle = $document->getStructure()->getStagedData()[$pageTitleProperty->getName()]; + } + + $document->setTitle($pageTitle); + } + + /** + * Find page-title property. + * + * @param ArticlePageDocument $document + * + * @return PropertyMetadata + */ + private function getPageTitleProperty(ArticlePageDocument $document) + { + $metadata = $this->structureMetadataFactory->getStructureMetadata( + 'article_page', + $document->getStructureType() + ); + + if ($metadata->hasPropertyWithTagName(self::PAGE_TITLE_TAG_NAME)) { + return $metadata->getPropertyByTagName(self::PAGE_TITLE_TAG_NAME); + } + + if ($metadata->hasProperty(self::PAGE_TITLE_PROPERTY_NAME)) { + return $metadata->getProperty(self::PAGE_TITLE_PROPERTY_NAME); + } + + return null; + } + + /** + * Set page-number to document on persist. + * + * @param PersistEvent $event + */ + public function setPageNumberOnPersist(PersistEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof ArticlePageDocument) { + return; + } + + $event->getAccessor()->set('pageNumber', $event->getNode()->getIndex() + 1); + } + + /** + * Set page-number to document on persist. + * + * @param HydrateEvent $event + */ + public function setPageNumberOnHydrate(HydrateEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof ArticlePageDocument) { + return; + } + + $event->getAccessor()->set('pageNumber', $event->getNode()->getIndex() + 1); + } +} diff --git a/Document/Subscriber/ArticleSubscriber.php b/Document/Subscriber/ArticleSubscriber.php index 2ae4e135d..1d1592194 100644 --- a/Document/Subscriber/ArticleSubscriber.php +++ b/Document/Subscriber/ArticleSubscriber.php @@ -12,6 +12,7 @@ namespace Sulu\Bundle\ArticleBundle\Document\Subscriber; use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; +use Sulu\Bundle\ArticleBundle\Document\ArticlePageDocument; use Sulu\Bundle\ArticleBundle\Document\Index\IndexerInterface; use Sulu\Component\DocumentManager\DocumentManagerInterface; use Sulu\Component\DocumentManager\Event\AbstractMappingEvent; @@ -73,7 +74,12 @@ public static function getSubscribedEvents() { return [ Events::PERSIST => [['handleScheduleIndex', -500]], - Events::REMOVE => [['handleRemove', -500], ['handleRemoveLive', -500]], + Events::REMOVE => [ + ['handleRemove', -500], + ['handleRemoveLive', -500], + ['handleRemovePage', -500], + ['handleRemovePageLive', -500], + ], Events::PUBLISH => [['handleScheduleIndexLive', 0], ['handleScheduleIndex', 0]], Events::UNPUBLISH => 'handleUnpublish', Events::REMOVE_DRAFT => ['handleScheduleIndex', -1024], @@ -90,7 +96,11 @@ public function handleScheduleIndex(AbstractMappingEvent $event) { $document = $event->getDocument(); if (!$document instanceof ArticleDocument) { - return; + if (!$document instanceof ArticlePageDocument) { + return; + } + + $document = $document->getParent(); } $this->documents[$document->getUuid()] = [ @@ -108,7 +118,11 @@ public function handleScheduleIndexLive(AbstractMappingEvent $event) { $document = $event->getDocument(); if (!$document instanceof ArticleDocument) { - return; + if (!$document instanceof ArticlePageDocument) { + return; + } + + $document = $document->getParent(); } $this->liveDocuments[$document->getUuid()] = [ @@ -128,13 +142,11 @@ public function handleFlush(FlushEvent $event) return; } - foreach ($this->documents as $document) { - $this->indexer->index( - $this->documentManager->find( - $document['uuid'], - $document['locale'] - ) - ); + foreach ($this->documents as $documentData) { + $document = $this->documentManager->find($documentData['uuid'], $documentData['locale']); + $this->documentManager->refresh($document, $documentData['locale']); + + $this->indexer->index($document); } $this->indexer->flush(); $this->documents = []; @@ -151,50 +163,70 @@ public function handleFlushLive(FlushEvent $event) return; } - foreach ($this->liveDocuments as $document) { - $this->liveIndexer->index( - $this->documentManager->find( - $document['uuid'], - $document['locale'] - ) - ); + foreach ($this->liveDocuments as $documentData) { + $document = $this->documentManager->find($documentData['uuid'], $documentData['locale']); + $this->documentManager->refresh($document, $documentData['locale']); + + $this->liveIndexer->index($document); } $this->liveIndexer->flush(); $this->liveDocuments = []; } /** - * Indexes for article-document in live index. + * Removes document from live index and unpublish document in default index. * - * @param AbstractMappingEvent $event + * @param UnpublishEvent $event */ - public function handleIndexLive(AbstractMappingEvent $event) + public function handleUnpublish(UnpublishEvent $event) { $document = $event->getDocument(); if (!$document instanceof ArticleDocument) { return; } - $this->liveIndexer->index($document); + $this->liveIndexer->remove($document); $this->liveIndexer->flush(); + + $this->indexer->setUnpublished($document->getUuid()); } /** - * Removes document from live index and unpublish document in default index. + * Reindex article if a page was removed. * - * @param UnpublishEvent $event + * @param RemoveEvent $event */ - public function handleUnpublish(UnpublishEvent $event) + public function handleRemovePage(RemoveEvent $event) { $document = $event->getDocument(); - if (!$document instanceof ArticleDocument) { + if (!$document instanceof ArticlePageDocument) { return; } - $this->liveIndexer->remove($document); - $this->liveIndexer->flush(); + $document = $document->getParent(); + $this->documents[$document->getUuid()] = [ + 'uuid' => $document->getUuid(), + 'locale' => $document->getLocale(), + ]; + } - $this->indexer->setUnpublished($document->getUuid()); + /** + * Reindex article live if a page was removed. + * + * @param RemoveEvent $event + */ + public function handleRemovePageLive(RemoveEvent $event) + { + $document = $event->getDocument(); + if (!$document instanceof ArticlePageDocument) { + return; + } + + $document = $document->getParent(); + $this->liveDocuments[$document->getUuid()] = [ + 'uuid' => $document->getUuid(), + 'locale' => $document->getLocale(), + ]; } /** diff --git a/Exception/ParameterNotAllowedException.php b/Exception/ParameterNotAllowedException.php new file mode 100644 index 000000000..10d9fb536 --- /dev/null +++ b/Exception/ParameterNotAllowedException.php @@ -0,0 +1,60 @@ +property = $property; + $this->class = $class; + } + + /** + * Returns property. + * + * @return string + */ + public function getProperty() + { + return $this->property; + } + + /** + * Returns class. + * + * @return string + */ + public function getClass() + { + return $this->class; + } +} diff --git a/Resources/config/routing_api.xml b/Resources/config/routing_api.xml index f7de44658..b3fab8ade 100644 --- a/Resources/config/routing_api.xml +++ b/Resources/config/routing_api.xml @@ -4,5 +4,6 @@ xsi:schemaLocation="http://friendsofsymfony.github.com/schema/rest https://raw.github.com/FriendsOfSymfony/FOSRestBundle/master/Resources/config/schema/routing/rest_routing-1.0.xsd"> + diff --git a/Resources/config/serializer/Document.ArticleDocument.xml b/Resources/config/serializer/Document.ArticleDocument.xml index 2a7fcc374..06dcbc0c4 100644 --- a/Resources/config/serializer/Document.ArticleDocument.xml +++ b/Resources/config/serializer/Document.ArticleDocument.xml @@ -29,5 +29,8 @@ + + ]]> + diff --git a/Resources/config/serializer/Document.ArticlePageDocument.xml b/Resources/config/serializer/Document.ArticlePageDocument.xml new file mode 100644 index 000000000..abf0cda8e --- /dev/null +++ b/Resources/config/serializer/Document.ArticlePageDocument.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 9c90710b9..912f0f4cb 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -113,6 +113,12 @@ + + + + + @@ -122,7 +128,7 @@ - @@ -130,6 +136,10 @@ + + + diff --git a/Resources/doc/installation.md b/Resources/doc/installation.md index e45942ed1..8ad1f1b9e 100644 --- a/Resources/doc/installation.md +++ b/Resources/doc/installation.md @@ -36,10 +36,14 @@ sulu_core: structure: default_type: article: "article_default" + article_page: "article_default" paths: article: path: "%kernel.root_dir%/Resources/templates/articles" type: "article" + article_page: + path: "%kernel.root_dir%/Resources/templates/articles" + type: "article_page" ongr_elasticsearch: connections: diff --git a/Tests/Functional/Controller/ArticleControllerTest.php b/Tests/Functional/Controller/ArticleControllerTest.php index c0eaa3623..246ca6b16 100644 --- a/Tests/Functional/Controller/ArticleControllerTest.php +++ b/Tests/Functional/Controller/ArticleControllerTest.php @@ -11,8 +11,11 @@ namespace Functional\Controller; +use ONGR\ElasticsearchBundle\Service\Manager; use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; +use Sulu\Bundle\ArticleBundle\Document\ArticleViewDocument; use Sulu\Bundle\ArticleBundle\Document\Index\IndexerInterface; +use Sulu\Bundle\ArticleBundle\Metadata\ArticleViewDocumentIdTrait; use Sulu\Bundle\MediaBundle\DataFixtures\ORM\LoadCollectionTypes; use Sulu\Bundle\MediaBundle\DataFixtures\ORM\LoadMediaTypes; use Sulu\Bundle\MediaBundle\Entity\Collection; @@ -28,6 +31,8 @@ */ class ArticleControllerTest extends SuluTestCase { + use ArticleViewDocumentIdTrait; + private static $typeMap = ['default' => 'blog', 'simple' => 'video']; /** @@ -89,6 +94,8 @@ public function testPost($title = 'Test-Article', $template = 'default') $this->assertEquals('2016-01-01', date('Y-m-d', strtotime($response['authored']))); $this->assertEquals($this->getTestUser()->getContact()->getId(), $response['author']); + $this->assertNotNull($this->findViewDocument($response['id'], 'de')); + return $response; } @@ -133,6 +140,8 @@ public function testPut($title = 'Sulu is awesome', $locale = 'de', $article = n $this->assertEquals('2016-01-01', date('Y-m-d', strtotime($response['authored']))); $this->assertEquals($this->getTestUser()->getContact()->getId(), $response['author']); + $this->assertNotNull($this->findViewDocument($response['id'], 'de')); + return $article; } @@ -673,6 +682,8 @@ public function testDelete() $response = json_decode($client->getResponse()->getContent(), true); $this->assertEquals(0, $response['total']); $this->assertCount(0, $response['_embedded']['articles']); + + $this->assertNull($this->findViewDocument($article['id'], 'de')); } public function testCDelete() @@ -695,6 +706,9 @@ public function testCDelete() $response = json_decode($client->getResponse()->getContent(), true); $this->assertEquals(0, $response['total']); $this->assertCount(0, $response['_embedded']['articles']); + + $this->assertNull($this->findViewDocument($article1['id'], 'de')); + $this->assertNull($this->findViewDocument($article2['id'], 'de')); } public function testCopyLocale() @@ -798,4 +812,12 @@ private function flush() $indexer = $this->getContainer()->get('sulu_article.elastic_search.article_indexer'); $indexer->flush(); } + + private function findViewDocument($uuid, $locale) + { + /** @var Manager $manager */ + $manager = $this->getContainer()->get('es.manager.default'); + + return $manager->find(ArticleViewDocument::class, $this->getViewDocumentId($uuid, $locale)); + } } diff --git a/Tests/Functional/Controller/ArticlePageControllerTest.php b/Tests/Functional/Controller/ArticlePageControllerTest.php new file mode 100644 index 000000000..f17eadcb0 --- /dev/null +++ b/Tests/Functional/Controller/ArticlePageControllerTest.php @@ -0,0 +1,193 @@ +purgeIndex(); + + $this->initPhpcr(); + + $collectionTypes = new LoadCollectionTypes(); + $collectionTypes->load($this->getEntityManager()); + $mediaTypes = new LoadMediaTypes(); + $mediaTypes->load($this->getEntityManager()); + } + + private function createArticle($title = 'Test-Article', $template = 'default_pages') + { + $client = $this->createAuthenticatedClient(); + $client->request( + 'POST', + '/api/articles?locale=de', + [ + 'title' => $title, + 'pageTitle' => $title, + 'template' => $template, + 'authored' => '2016-01-01', + 'author' => $this->getTestUser()->getContact()->getId(), + ] + ); + + return json_decode($client->getResponse()->getContent(), true); + } + + private function getArticle($uuid) + { + $client = $this->createAuthenticatedClient(); + $client->request('GET', '/api/articles/' . $uuid . '?locale=de'); + + return json_decode($client->getResponse()->getContent(), true); + } + + private function post($article, $pageTitle = 'Test-Page') + { + $client = $this->createAuthenticatedClient(); + $client->request( + 'POST', + '/api/articles/' . $article['id'] . '/pages?locale=de', + [ + 'pageTitle' => $pageTitle, + 'author' => $this->getTestUser()->getContact()->getId(), + ] + ); + $this->assertHttpStatusCode(200, $client->getResponse()); + + return json_decode($client->getResponse()->getContent(), true); + } + + public function testPost($title = 'Test-Article', $pageTitle = 'Test-Page', $template = 'default_pages') + { + $article = $this->createArticle($title, $template); + $response = $this->post($article, $pageTitle); + + $this->assertEquals($title, $response['title']); + $this->assertEquals($pageTitle, $response['pageTitle']); + $this->assertEquals($template, $response['template']); + $this->assertEquals(2, $response['pageNumber']); + + $article = $this->getArticle($article['id']); + $this->assertCount(1, $article['pages']); + $this->assertEquals($response['id'], reset($article['pages'])['id']); + + $articleViewDocument = $this->findViewDocument($article['id'], 'de'); + $this->assertCount(1, $articleViewDocument->getPages()); + $this->assertEquals(2, $articleViewDocument->getPages()[0]->pageNumber); + $this->assertEquals($pageTitle, $articleViewDocument->getPages()[0]->title); + $this->assertEquals($response['id'], $articleViewDocument->getPages()[0]->uuid); + } + + public function testGet($title = 'Test-Article', $pageTitle = 'Test-Page', $template = 'default_pages') + { + $article = $this->createArticle($title, $template); + $page = $this->post($article, $pageTitle); + + $client = $this->createAuthenticatedClient(); + $client->request('GET', '/api/articles/' . $article['id'] . '/pages/' . $page['id'] . '?locale=de'); + + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertHttpStatusCode(200, $client->getResponse()); + + $this->assertEquals($title, $response['title']); + $this->assertEquals($pageTitle, $response['pageTitle']); + $this->assertEquals($template, $response['template']); + $this->assertEquals(2, $response['pageNumber']); + } + + public function testPut($title = 'Test-Article', $pageTitle = 'New-Page-Title', $template = 'default_pages') + { + $article = $this->createArticle($title, $template); + $page = $this->post($article); + + $client = $this->createAuthenticatedClient(); + $client->request( + 'PUT', + '/api/articles/' . $article['id'] . '/pages/' . $page['id'] . '?locale=de', + [ + 'pageTitle' => $pageTitle, + 'article' => 'Sulu is awesome', + 'author' => $this->getTestUser()->getContact()->getId(), + ] + ); + + $response = json_decode($client->getResponse()->getContent(), true); + $this->assertHttpStatusCode(200, $client->getResponse()); + + $this->assertEquals($title, $response['title']); + $this->assertEquals($pageTitle, $response['pageTitle']); + $this->assertEquals($template, $response['template']); + $this->assertEquals('Sulu is awesome', $response['article']); + $this->assertEquals(2, $response['pageNumber']); + + $articleViewDocument = $this->findViewDocument($article['id'], 'de'); + $this->assertCount(1, $articleViewDocument->getPages()); + $this->assertEquals(2, $articleViewDocument->getPages()[0]->pageNumber); + $this->assertEquals($pageTitle, $articleViewDocument->getPages()[0]->title); + $this->assertEquals($response['id'], $articleViewDocument->getPages()[0]->uuid); + } + + public function testDelete() + { + $article = $this->createArticle(); + $page = $this->post($article); + + $client = $this->createAuthenticatedClient(); + $client->request('DELETE', '/api/articles/' . $article['id'] . '/pages/' . $page['id']); + + $this->assertHttpStatusCode(204, $client->getResponse()); + + $article = $this->getArticle($article['id']); + $this->assertCount(0, $article['pages']); + + $articleViewDocument = $this->findViewDocument($article['id'], 'de'); + $this->assertCount(0, $articleViewDocument->getPages()); + } + + private function purgeIndex() + { + /** @var IndexerInterface $indexer */ + $indexer = $this->getContainer()->get('sulu_article.elastic_search.article_indexer'); + $indexer->clear(); + } + + /** + * @param $uuid + * @param $locale + * + * @return ArticleViewDocumentInterface + */ + private function findViewDocument($uuid, $locale) + { + /** @var Manager $manager */ + $manager = $this->getContainer()->get('es.manager.default'); + + return $manager->find(ArticleViewDocument::class, $this->getViewDocumentId($uuid, $locale)); + } +} diff --git a/Tests/Unit/Document/Subscriber/ArticlePageSubscriberTest.php b/Tests/Unit/Document/Subscriber/ArticlePageSubscriberTest.php new file mode 100644 index 000000000..45eaeeefc --- /dev/null +++ b/Tests/Unit/Document/Subscriber/ArticlePageSubscriberTest.php @@ -0,0 +1,163 @@ +factory = $this->prophesize(StructureMetadataFactoryInterface::class); + $this->metadata = $this->prophesize(StructureMetadata::class); + $this->document = $this->prophesize(ArticlePageDocument::class); + + $this->document->getStructureType()->willReturn('default'); + $this->factory->getStructureMetadata('article_page', 'default')->willReturn($this->metadata->reveal()); + + $this->subscriber = new ArticlePageSubscriber($this->factory->reveal()); + } + + private function createEvent($className, $node = null, $accessor = null) + { + $event = $this->prophesize($className); + $event->getDocument()->willReturn($this->document->reveal()); + + if ($node) { + $event->getNode()->willReturn($node); + } + + if ($accessor) { + $event->getAccessor()->willReturn($accessor); + } + + return $event->reveal(); + } + + public function testSetTitleOnPersist() + { + $event = $this->createEvent(PersistEvent::class); + + $property = $this->prophesize(PropertyMetadata::class); + $property->getName()->willReturn('pageTitle'); + + $this->metadata->hasPropertyWithTagName(ArticlePageSubscriber::PAGE_TITLE_TAG_NAME)->willReturn(true); + $this->metadata->hasProperty(ArticlePageSubscriber::PAGE_TITLE_PROPERTY_NAME)->willReturn(false); + $this->metadata->getPropertyByTagName(ArticlePageSubscriber::PAGE_TITLE_TAG_NAME)->willReturn( + $property->reveal() + ); + + $structure = $this->prophesize(StructureInterface::class); + $structure->getStagedData()->willReturn(['pageTitle' => 'Test title']); + $this->document->getStructure()->willReturn($structure->reveal()); + + $this->document->setTitle('Test title')->shouldBeCalled(); + + $this->subscriber->setTitleOnPersist($event); + } + + public function testSetTitleOnPersistWithoutTag() + { + $event = $this->createEvent(PersistEvent::class); + + $property = $this->prophesize(PropertyMetadata::class); + $property->getName()->willReturn('pageTitle'); + + $this->metadata->hasPropertyWithTagName(ArticlePageSubscriber::PAGE_TITLE_TAG_NAME)->willReturn(false); + $this->metadata->hasProperty(ArticlePageSubscriber::PAGE_TITLE_PROPERTY_NAME)->willReturn(true); + $this->metadata->getProperty(ArticlePageSubscriber::PAGE_TITLE_PROPERTY_NAME)->willReturn( + $property->reveal() + ); + + $structure = $this->prophesize(StructureInterface::class); + $structure->getStagedData()->willReturn(['pageTitle' => 'Test title']); + $this->document->getStructure()->willReturn($structure->reveal()); + + $this->document->setTitle('Test title')->shouldBeCalled(); + + $this->subscriber->setTitleOnPersist($event); + } + + public function testSetTitleOnPersistWithoutTagAndProperty() + { + $event = $this->createEvent(PersistEvent::class); + + $property = $this->prophesize(PropertyMetadata::class); + $property->getName()->willReturn('pageTitle'); + + $this->metadata->hasPropertyWithTagName(ArticlePageSubscriber::PAGE_TITLE_TAG_NAME)->willReturn(false); + $this->metadata->hasProperty(ArticlePageSubscriber::PAGE_TITLE_PROPERTY_NAME)->willReturn(false); + + $this->document->setTitle(Argument::type('string'))->shouldBeCalled(); + + $this->subscriber->setTitleOnPersist($event); + } + + public function testSetPageNumberOnPersist() + { + $node = $this->prophesize(NodeInterface::class); + $node->getIndex()->willReturn(1); + + $accessor = $this->prophesize(DocumentAccessor::class); + $accessor->set('pageNumber', 2)->shouldBeCalled(); + + $event = $this->createEvent(PersistEvent::class, $node->reveal(), $accessor->reveal()); + + $this->subscriber->setPageNumberOnPersist($event); + } + + public function testSetPageNumberOnHydrate() + { + $node = $this->prophesize(NodeInterface::class); + $node->getIndex()->willReturn(1); + + $accessor = $this->prophesize(DocumentAccessor::class); + $accessor->set('pageNumber', 2)->shouldBeCalled(); + + $event = $this->createEvent(HydrateEvent::class, $node->reveal(), $accessor->reveal()); + + $this->subscriber->setPageNumberOnHydrate($event); + } +} diff --git a/Tests/Unit/Document/Subscriber/ArticleSubscriberTest.php b/Tests/Unit/Document/Subscriber/ArticleSubscriberTest.php index 46bc2592e..9b736e4cb 100644 --- a/Tests/Unit/Document/Subscriber/ArticleSubscriberTest.php +++ b/Tests/Unit/Document/Subscriber/ArticleSubscriberTest.php @@ -114,6 +114,7 @@ public function testHandleFlush() $this->articleSubscriber->handleScheduleIndex($event); $this->documentManager->find($this->uuid, $this->locale)->willReturn($this->document->reveal()); + $this->documentManager->refresh($this->document->reveal(), $this->locale)->willReturn($this->document->reveal()); $this->articleSubscriber->handleFlush($this->prophesize(FlushEvent::class)->reveal()); @@ -129,6 +130,7 @@ public function testHandleFlushLive() $this->articleSubscriber->handleScheduleIndexLive($event); $this->documentManager->find($this->uuid, $this->locale)->willReturn($this->document->reveal()); + $this->documentManager->refresh($this->document->reveal(), $this->locale)->willReturn($this->document->reveal()); $this->articleSubscriber->handleFlushLive($this->prophesize(FlushEvent::class)->reveal()); diff --git a/Tests/app/Resources/articles/default_pages.xml b/Tests/app/Resources/articles/default_pages.xml new file mode 100644 index 000000000..69cd4a727 --- /dev/null +++ b/Tests/app/Resources/articles/default_pages.xml @@ -0,0 +1,27 @@ + + diff --git a/Tests/app/config/config.yml b/Tests/app/config/config.yml index 2001fcbcd..597909042 100644 --- a/Tests/app/config/config.yml +++ b/Tests/app/config/config.yml @@ -1,17 +1,21 @@ # Doctrine Configuration doctrine: dbal: - dbname: "su_articles_test" + dbname: "su_articles_test" sulu_core: content: structure: default_type: article: "default" + article_page: "default" paths: article: path: "%kernel.root_dir%/Resources/articles" type: "article" + article_page: + path: "%kernel.root_dir%/Resources/articles" + type: "article_page" ongr_elasticsearch: connections: