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 @@
+
+
+
+ default_pages
+
+ ::default
+ SuluWebsiteBundle:Default:index
+ 2400
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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: