diff --git a/Content/ArticleDataProvider.php b/Content/ArticleDataProvider.php
index 1664db222..d5d206adb 100644
--- a/Content/ArticleDataProvider.php
+++ b/Content/ArticleDataProvider.php
@@ -55,6 +55,11 @@ class ArticleDataProvider implements DataProviderInterface, DataProviderAliasInt
*/
private $referenceStore;
+ /**
+ * @var ArticleResourceItemFactory
+ */
+ protected $articleResourceItemFactory;
+
/**
* @var string
*/
@@ -70,6 +75,7 @@ class ArticleDataProvider implements DataProviderInterface, DataProviderAliasInt
* @param DocumentManagerInterface $documentManager
* @param LazyLoadingValueHolderFactory $proxyFactory
* @param ReferenceStoreInterface $referenceStore
+ * @param ArticleResourceItemFactory $articleResourceItemFactory
* @param string $articleDocumentClass
* @param int $defaultLimit
*/
@@ -78,6 +84,7 @@ public function __construct(
DocumentManagerInterface $documentManager,
LazyLoadingValueHolderFactory $proxyFactory,
ReferenceStoreInterface $referenceStore,
+ ArticleResourceItemFactory $articleResourceItemFactory,
$articleDocumentClass,
$defaultLimit
) {
@@ -85,6 +92,7 @@ public function __construct(
$this->documentManager = $documentManager;
$this->proxyFactory = $proxyFactory;
$this->referenceStore = $referenceStore;
+ $this->articleResourceItemFactory = $articleResourceItemFactory;
$this->articleDocumentClass = $articleDocumentClass;
$this->defaultLimit = $defaultLimit;
}
@@ -180,10 +188,7 @@ public function resolveResourceItems(
$this->referenceStore->add($document->getUuid());
$uuids[] = $document->getUuid();
- $result[] = new ArticleResourceItem(
- $document,
- $this->getResource($document->getUuid(), $document->getLocale())
- );
+ $result[] = $this->articleResourceItemFactory->createResourceItem($document);
}
return new DataProviderResult($result, $this->hasNextPage($queryResult, $limit, $page, $pageSize), $uuids);
diff --git a/Content/ArticleResourceItem.php b/Content/ArticleResourceItem.php
index 79f8660c9..e01f8cd0c 100644
--- a/Content/ArticleResourceItem.php
+++ b/Content/ArticleResourceItem.php
@@ -132,6 +132,16 @@ public function getPublished()
return $this->article->getPublished();
}
+ /**
+ * Returns authored.
+ *
+ * @return \DateTime
+ */
+ public function getAuthored()
+ {
+ return $this->article->getAuthored();
+ }
+
/**
* Returns excerpt.
*
diff --git a/Content/ArticleResourceItemFactory.php b/Content/ArticleResourceItemFactory.php
new file mode 100644
index 000000000..8c8061a01
--- /dev/null
+++ b/Content/ArticleResourceItemFactory.php
@@ -0,0 +1,88 @@
+documentManager = $documentManager;
+ $this->proxyFactory = $proxyFactory;
+ }
+
+ /**
+ * Creates and returns article source item with proxy document.
+ *
+ * @param ArticleViewDocumentInterface $articleViewDocument
+ *
+ * @return ArticleResourceItem
+ */
+ public function createResourceItem(ArticleViewDocumentInterface $articleViewDocument)
+ {
+ return new ArticleResourceItem(
+ $articleViewDocument,
+ $this->getResource($articleViewDocument->getUuid(), $articleViewDocument->getLocale())
+ );
+ }
+
+ /**
+ * Returns Proxy document for uuid.
+ *
+ * @param string $uuid
+ * @param string $locale
+ *
+ * @return object
+ */
+ private function getResource($uuid, $locale)
+ {
+ return $this->proxyFactory->createProxy(
+ ArticleDocument::class,
+ function (
+ &$wrappedObject,
+ LazyLoadingInterface $proxy,
+ $method,
+ array $parameters,
+ &$initializer
+ ) use ($uuid, $locale) {
+ $initializer = null;
+ $wrappedObject = $this->documentManager->find($uuid, $locale);
+
+ return true;
+ }
+ );
+ }
+}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 2e8e5a32e..588e330c6 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -86,6 +86,17 @@ public function getConfigTreeBuilder()
->end()
->end()
->scalarNode('display_tab_all')->defaultTrue()->info("Display tab 'all' in list view")->end()
+ ->arrayNode('search_fields')
+ ->prototype('scalar')->end()->defaultValue([
+ 'title',
+ 'excerpt.title',
+ 'excerpt.description',
+ 'excerpt.seo.title',
+ 'excerpt.seo.description',
+ 'excerpt.seo.keywords',
+ 'teaser_description',
+ ])
+ ->end()
->end();
return $treeBuilder;
diff --git a/DependencyInjection/SuluArticleExtension.php b/DependencyInjection/SuluArticleExtension.php
index 5574e6be6..274f62c60 100644
--- a/DependencyInjection/SuluArticleExtension.php
+++ b/DependencyInjection/SuluArticleExtension.php
@@ -21,7 +21,6 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
-use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
@@ -139,6 +138,7 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter('sulu_article.view_document.article.class', $config['documents']['article']['view']);
$container->setParameter('sulu_article.display_tab_all', $config['display_tab_all']);
$container->setParameter('sulu_article.smart_content.default_limit', $config['smart_content']['default_limit']);
+ $container->setParameter('sulu_article.search_fields', $config['search_fields']);
$container->setParameter(
'sulu_article.content-type.article.template',
diff --git a/Document/Repository/ArticleViewDocumentRepository.php b/Document/Repository/ArticleViewDocumentRepository.php
new file mode 100644
index 000000000..b88ffc8ed
--- /dev/null
+++ b/Document/Repository/ArticleViewDocumentRepository.php
@@ -0,0 +1,159 @@
+searchManager = $searchManager;
+ $this->articleDocumentClass = $articleDocumentClass;
+ $this->repository = $this->searchManager->getRepository($this->articleDocumentClass);
+ $this->searchFields = $searchFields;
+ }
+
+ /**
+ * Finds recent articles for given parameters sorted by field `authored`.
+ *
+ * @param null|string $excludeUuid
+ * @param int $limit
+ * @param null|array $types
+ * @param null|string $locale
+ *
+ * @return DocumentIterator
+ */
+ public function findRecent($excludeUuid = null, $limit = self::DEFAULT_LIMIT, array $types = null, $locale = null)
+ {
+ $search = $this->createSearch($limit, $types, $locale);
+
+ if ($excludeUuid) {
+ $search->addQuery(new TermQuery('uuid', $excludeUuid), BoolQuery::MUST_NOT);
+ }
+
+ $search->addSort(new FieldSort('authored', FieldSort::DESC));
+
+ return $this->repository->findDocuments($search);
+ }
+
+ /**
+ * Finds similar articles for given `uuid` with given parameters.
+ *
+ * @param string $uuid
+ * @param int $limit
+ * @param null|array $types
+ * @param null|string $locale
+ *
+ * @return DocumentIterator
+ */
+ public function findSimilar($uuid, $limit = self::DEFAULT_LIMIT, array $types = null, $locale = null)
+ {
+ $search = $this->createSearch($limit, $types, $locale);
+
+ $search->addQuery(
+ new MoreLikeThisQuery(
+ null,
+ [
+ 'fields' => $this->searchFields,
+ 'min_term_freq' => 1,
+ 'min_doc_freq' => 2,
+ 'ids' => [$this->getViewDocumentId($uuid, $locale)],
+ ]
+ )
+ );
+
+ return $this->repository->findDocuments($search);
+ }
+
+ /**
+ * Creates search with default queries (size, locale, types).
+ *
+ * @param int $limit
+ * @param null|array $types
+ * @param null|string $locale
+ *
+ * @return Search
+ */
+ private function createSearch($limit, array $types = null, $locale = null)
+ {
+ $search = $this->repository->createSearch();
+
+ // set size
+ $search->setSize($limit);
+
+ // filter by locale if provided
+ if ($locale) {
+ $search->addQuery(new TermQuery('locale', $locale), BoolQuery::FILTER);
+ }
+
+ // filter by types if provided
+ if ($types) {
+ $typesQuery = new BoolQuery();
+ foreach ($types as $type) {
+ $typesQuery->add(new TermQuery('type', $type), BoolQuery::SHOULD);
+ }
+ $search->addQuery($typesQuery);
+ }
+
+ return $search;
+ }
+}
diff --git a/Exception/ArticleInRequestNotFoundException.php b/Exception/ArticleInRequestNotFoundException.php
new file mode 100644
index 000000000..fe029a2fc
--- /dev/null
+++ b/Exception/ArticleInRequestNotFoundException.php
@@ -0,0 +1,23 @@
+
+
%sulu_article.view_document.article.class%
%sulu_article.smart_content.default_limit%
@@ -230,6 +231,7 @@
+
%sulu_article.view_document.article.class%
%sulu_article.smart_content.default_limit%
@@ -295,5 +297,32 @@
+
+
+
+
+
+
+
+
+
+
+ %sulu_article.view_document.article.class%
+ %sulu_article.search_fields%
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/doc/README.md b/Resources/doc/README.md
index dd2b75dc3..071b2efb7 100644
--- a/Resources/doc/README.md
+++ b/Resources/doc/README.md
@@ -7,3 +7,4 @@ This documentation covers basic-topics to install and use this bundle.
* [Installation](installation.md)
* [Routing](routing.md)
* [Content-Types](content-types.md)
+* [Twig-Extensions](twig-extensions.md)
diff --git a/Resources/doc/content-types.md b/Resources/doc/content-types.md
index 7741a924f..b585ba271 100644
--- a/Resources/doc/content-types.md
+++ b/Resources/doc/content-types.md
@@ -49,7 +49,7 @@ the articles.
### Returns
-The content-type returns a list of `ArticleViewDocumentInterface` instances.
+The content-type returns a list of `ArticleResourceItem` instances.
## Teaser-Selection
diff --git a/Resources/doc/installation.md b/Resources/doc/installation.md
index b57b19c4f..dab96722d 100644
--- a/Resources/doc/installation.md
+++ b/Resources/doc/installation.md
@@ -118,10 +118,21 @@ php bin/console ongr:es:index:create --manager=live
```yml
# app/config/config.yml
+# Default configuration for extension with alias: "sulu_article"
sulu_article:
+ smart_content:
+ default_limit: 100
+ content_types:
+ article:
+ template: 'SuluArticleBundle:Template:content-types/article-selection.html.twig'
+ page_tree_route:
+ template: 'SuluArticleBundle:Template:content-types/page-tree-route.html.twig'
+ page_route_cascade: request # One of "request"; "task"; "off"
documents:
article:
- view: Sulu\Bundle\ArticleBundle\Document\ArticleViewDocument
+ view: Sulu\Bundle\ArticleBundle\Document\ArticleViewDocument
+ article_page:
+ view: Sulu\Bundle\ArticleBundle\Document\ArticlePageViewObject
types:
# Prototype
@@ -130,6 +141,16 @@ sulu_article:
# Display tab 'all' in list view
display_tab_all: true
+ search_fields:
+
+ # Defaults:
+ - title
+ - excerpt.title
+ - excerpt.description
+ - excerpt.seo.title
+ - excerpt.seo.description
+ - excerpt.seo.keywords
+ - teaser_description
```
diff --git a/Resources/doc/twig-extensions.md b/Resources/doc/twig-extensions.md
new file mode 100644
index 000000000..d8c7086a3
--- /dev/null
+++ b/Resources/doc/twig-extensions.md
@@ -0,0 +1,47 @@
+# Twig Extensions
+
+Twig Extensions
+
+* Load recent articles: `sulu_article_load_recent`
+* Load similar articles: `sulu_article_load_similar`
+
+## `sulu_article_load_recent`
+
+Returns recent published articles, filter by given parameters.
+
+### Arguments
+
+- **limit**: *integer* - optional: set the limit - default: 5
+- **types**: *array* - optional: filter for article types - default: type of the requested article or null
+- **locale**: *string* - optional: filter for locale - default: locale of the request
+
+### Returns
+
+The content-type returns a list of `ArticleResourceItem` instances.
+
+### Example
+
+```twig
+{% set articles = sulu_article_load_recent(3, ['blog']) %}
+```
+
+## `sulu_article_load_similar`
+
+Returns similar articles compared to the requested one.
+Note: Which fields are included in this request can be configured with `sulu_article.search_fields` config parameter.
+
+### Arguments
+
+- **limit**: *integer* - optional: set the limit - default: 5
+- **types**: *array* - optional: filter for article types - default: type of the requested article
+- **locale**: *string* - optional: filter for locale - default: locale of the request
+
+### Returns
+
+The content-type returns a list of `ArticleResourceItem` instances.
+
+### Example
+
+```twig
+{% set articles = sulu_article_load_similar(5, ['blog']) %}
+```
diff --git a/Tests/Functional/Twig/ArticleViewDocumentTwigExtensionTest.php b/Tests/Functional/Twig/ArticleViewDocumentTwigExtensionTest.php
new file mode 100644
index 000000000..5e8f162ae
--- /dev/null
+++ b/Tests/Functional/Twig/ArticleViewDocumentTwigExtensionTest.php
@@ -0,0 +1,97 @@
+initPhpcr();
+
+ /** @var Manager $manager */
+ $manager = $this->getContainer()->get('es.manager.default');
+ $manager->dropAndCreateIndex();
+ $manager = $this->getContainer()->get('es.manager.live');
+ $manager->dropAndCreateIndex();
+ }
+
+ public function testFindMethods()
+ {
+ $items = [
+ $this->createArticle('XXX 1'),
+ $this->createArticle('XXX 2'),
+ $this->createArticle('YYY 3'),
+ $this->createArticle('YYY 4'),
+ ];
+
+ $fakeRequest = Request::create('/', 'GET');
+ $fakeRequest->setLocale('de');
+ $fakeRequest->attributes->set('object', $this->getArticleDocument($items[0]['id']));
+
+ $requestStack = $this->getContainer()->get('request_stack');
+ $requestStack->push($fakeRequest);
+
+ // test 'loadRecent' => all others should be returned
+ $result = $this->getTwigExtension()->loadRecent();
+ $this->assertCount(count($items) - 1, $result);
+
+ // test 'loadSimilar' => only article with title 'XXX 2' should be returned
+ $result = $this->getTwigExtension()->loadSimilar();
+ $this->assertCount(1, $result);
+ $this->assertEquals($items[1]['title'], $result[0]->getTitle());
+ }
+
+ private function createArticle($title = 'Test-Article', $template = 'default')
+ {
+ $client = $this->createAuthenticatedClient();
+ $client->request(
+ 'POST',
+ '/api/articles?locale=' . self::LOCALE . '&action=publish',
+ ['title' => $title, 'template' => $template]
+ );
+
+ return json_decode($client->getResponse()->getContent(), true);
+ }
+
+ /**
+ * @param string $uuid
+ *
+ * @return ArticleDocument
+ */
+ private function getArticleDocument($uuid)
+ {
+ $documentManager = $this->getContainer()->get('sulu_document_manager.document_manager');
+
+ return $documentManager->find($uuid, self::LOCALE);
+ }
+
+ /**
+ * @return ArticleViewDocumentTwigExtension
+ */
+ private function getTwigExtension()
+ {
+ return $this->getContainer()->get('sulu_article.twig.view_document_repository');
+ }
+}
diff --git a/Tests/Unit/Factory/ArticleResourceItemFactoryTest.php b/Tests/Unit/Factory/ArticleResourceItemFactoryTest.php
new file mode 100644
index 000000000..4c38a6780
--- /dev/null
+++ b/Tests/Unit/Factory/ArticleResourceItemFactoryTest.php
@@ -0,0 +1,74 @@
+getArticleDocument();
+ $articleViewDocument = $this->getArticleViewDocument($articleDocument);
+ $documentManager = $this->prophesize(DocumentManager::class);
+ $proxyFactory = $this->prophesize(LazyLoadingValueHolderFactory::class);
+
+ $articleResourceItemFactory = new ArticleResourceItemFactory(
+ $documentManager->reveal(),
+ $proxyFactory->reveal()
+ );
+
+ $proxyFactory->createProxy(Argument::cetera())
+ ->willReturn(
+ $this->prophesize(ArticleDocument::class)->willImplement(VirtualProxyInterface::class)->reveal()
+ );
+
+ $result = $articleResourceItemFactory->createResourceItem($articleViewDocument);
+
+ $this->assertInstanceOf(VirtualProxyInterface::class, $result->getResource());
+ $this->assertInstanceOf(ArticleDocument::class, $result->getResource());
+ $this->assertInstanceOf(ArticleResourceItem::class, $result);
+ $this->assertEquals($result->getUuid(), $articleDocument->getUuid());
+ }
+
+ /**
+ * @return ArticleDocument
+ */
+ private function getArticleDocument()
+ {
+ $articleDocument = new ArticleDocument();
+ $articleDocument->setUuid('123-123-123');
+ $articleDocument->setLocale('de');
+
+ return $articleDocument;
+ }
+
+ /**
+ * @param ArticleDocument $articleDocument
+ *
+ * @return ArticleViewDocument
+ */
+ private function getArticleViewDocument(ArticleDocument $articleDocument)
+ {
+ $articleViewDocument = new ArticleViewDocument($articleDocument->getUuid());
+ $articleViewDocument->setLocale($articleDocument->getLocale());
+
+ return $articleViewDocument;
+ }
+}
diff --git a/Tests/Unit/Repository/ArticleViewDocumentRepositoryTest.php b/Tests/Unit/Repository/ArticleViewDocumentRepositoryTest.php
new file mode 100644
index 000000000..d1dd2ed0f
--- /dev/null
+++ b/Tests/Unit/Repository/ArticleViewDocumentRepositoryTest.php
@@ -0,0 +1,191 @@
+search = $this->prophesize(Search::class);
+
+ $this->repository = $this->prophesize(Repository::class);
+ $this->repository->createSearch()->willReturn(new Search());
+
+ $this->searchManager = $this->prophesize(Manager::class);
+ $this->searchManager->getRepository(ArticleViewDocument::class)->willReturn($this->repository->reveal());
+
+ $this->articleViewDocumentRepository = new ArticleViewDocumentRepository(
+ $this->searchManager->reveal(),
+ ArticleViewDocument::class,
+ ['title', 'teaser_description']
+ );
+ }
+
+ public function dataProvider()
+ {
+ return [
+ [],
+ ['123-123-123', ['blog', 'article']],
+ ['321-321-321', ['blog'], 'en', 10],
+ ];
+ }
+
+ /**
+ * @dataProvider dataProvider
+ *
+ * @param string $uuid
+ * @param array $types
+ * @param string $locale
+ * @param int $limit
+ */
+ public function testFindSimilar($uuid = '123-123-123', array $types = ['blog'], $locale = 'de', $limit = 12)
+ {
+ $termQuery = [];
+ foreach ($types as $type) {
+ $termQuery[] = [
+ 'term' => ['type' => $type],
+ ];
+ }
+
+ $expectedSearch = [
+ 'bool' => [
+ 'filter' => [
+ [
+ 'term' => ['locale' => $locale],
+ ],
+ ],
+ 'must' => [
+ [
+ 'bool' => [
+ 'should' => $termQuery,
+ ],
+ ],
+ [
+ 'more_like_this' => [
+ 'like' => null,
+ 'fields' => ['title', 'teaser_description'],
+ 'min_term_freq' => 1,
+ 'min_doc_freq' => 2,
+ 'ids' => [$uuid . '-' . $locale],
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $documentIterator = $this->prophesize(DocumentIterator::class);
+
+ $this->repository->findDocuments(Argument::that(function(Search $search) use ($expectedSearch, $limit) {
+ $this->assertEquals($expectedSearch, $search->getQueries()->toArray());
+ $this->assertEquals($limit, $search->getSize());
+ $this->assertCount(0, $search->getSorts());
+
+ return true;
+ }))->willReturn($documentIterator->reveal());
+
+ $this->assertSame(
+ $documentIterator->reveal(),
+ $this->articleViewDocumentRepository->findSimilar($uuid, $limit, $types, $locale)
+ );
+ }
+
+ /**
+ * @dataProvider dataProvider
+ *
+ * @param string $excludeUuid
+ * @param array $types
+ * @param string $locale
+ * @param int $limit
+ */
+ public function testFindRecent(
+ $excludeUuid = '123-123-123',
+ array $types = ['blog'],
+ $locale = 'de',
+ $limit = 12
+ ) {
+ $termQuery = [];
+ foreach ($types as $type) {
+ $termQuery[] = [
+ 'term' => ['type' => $type],
+ ];
+ }
+
+ $expectedSearch = [
+ 'bool' => [
+ 'filter' => [
+ [
+ 'term' => ['locale' => $locale],
+ ],
+ ],
+ 'must' => [
+ [
+ 'bool' => [
+ 'should' => $termQuery,
+ ],
+ ],
+ ],
+ 'must_not' => [
+ [
+ 'term' => [
+ 'uuid' => $excludeUuid,
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $documentIterator = $this->prophesize(DocumentIterator::class);
+
+ $this->repository->findDocuments(Argument::that(function(Search $search) use ($expectedSearch, $limit) {
+ $this->assertEquals($expectedSearch, $search->getQueries()->toArray());
+ $this->assertEquals($limit, $search->getSize());
+ $this->assertCount(1, $search->getSorts());
+ $this->assertEquals(new FieldSort('authored', FieldSort::DESC), current($search->getSorts()));
+
+ return true;
+ }))->willReturn($documentIterator->reveal());
+
+ $this->assertSame(
+ $documentIterator->reveal(),
+ $this->articleViewDocumentRepository->findRecent($excludeUuid, $limit, $types, $locale)
+ );
+ }
+}
diff --git a/Tests/Unit/Twig/ArticleViewDocumentTwigExtensionTest.php b/Tests/Unit/Twig/ArticleViewDocumentTwigExtensionTest.php
new file mode 100644
index 000000000..8acb9ba10
--- /dev/null
+++ b/Tests/Unit/Twig/ArticleViewDocumentTwigExtensionTest.php
@@ -0,0 +1,225 @@
+getArticleDocuments();
+ $articleViewDocuments = $this->getArticleViewDocuments($articleDocuments);
+ $articleResourceItems = $this->getArticleResourceItems($articleDocuments, $articleViewDocuments);
+ $documentIterator = $this->getDocumentIterator($articleViewDocuments);
+ $metadataFactory = $this->getMetadataFactory();
+
+ $articleViewDocumentRepository = $this->prophesize(ArticleViewDocumentRepository::class);
+ $articleViewDocumentRepository->findSimilar(
+ $articleDocuments[0]->getUuid(),
+ 5,
+ [
+ $articleDocuments[0]->getStructureType(),
+ ],
+ $articleDocuments[0]->getLocale()
+ )->willReturn($documentIterator);
+
+ $articleResourceItemFactory = $this->prophesize(ArticleResourceItemFactory::class);
+ $articleResourceItemFactory->createResourceItem($articleViewDocuments[1])->willReturn($articleResourceItems[1]);
+ $articleResourceItemFactory->createResourceItem($articleViewDocuments[2])->willReturn($articleResourceItems[2]);
+
+ $request = $this->prophesize(Request::class);
+ $request->get('object')->willReturn($articleDocuments[0]);
+ $request->getLocale()->willReturn('de');
+
+ $requestStack = $this->prophesize(RequestStack::class);
+ $requestStack->getCurrentRequest()->wilLReturn($request);
+
+ $referenceStore = $this->prophesize(ReferenceStore::class);
+ $referenceStore->add($articleDocuments[1]->getUuid())->shouldBeCalled();
+ $referenceStore->add($articleDocuments[2]->getUuid())->shouldBeCalled();
+
+ $extension = new ArticleViewDocumentTwigExtension(
+ $articleViewDocumentRepository->reveal(),
+ $articleResourceItemFactory->reveal(),
+ $referenceStore->reveal(),
+ $metadataFactory->reveal(),
+ $requestStack->reveal()
+ );
+
+ $this->assertEquals(
+ [
+ $articleResourceItems[1],
+ $articleResourceItems[2],
+ ],
+ $extension->loadSimilar()
+ );
+ }
+
+ public function testFindRecent()
+ {
+ $articleDocuments = $this->getArticleDocuments();
+ $articleViewDocuments = $this->getArticleViewDocuments($articleDocuments);
+ $articleResourceItems = $this->getArticleResourceItems($articleDocuments, $articleViewDocuments);
+ $documentIterator = $this->getDocumentIterator($articleViewDocuments);
+ $metadataFactory = $this->getMetadataFactory();
+
+ $articleViewDocumentRepository = $this->prophesize(ArticleViewDocumentRepository::class);
+ $articleViewDocumentRepository->findRecent(
+ $articleDocuments[0]->getUuid(),
+ 5,
+ [
+ $articleDocuments[0]->getStructureType(),
+ ],
+ $articleDocuments[0]->getLocale()
+ )->willReturn($documentIterator);
+
+ $articleResourceItemFactory = $this->prophesize(ArticleResourceItemFactory::class);
+ $articleResourceItemFactory->createResourceItem($articleViewDocuments[1])->willReturn($articleResourceItems[1]);
+ $articleResourceItemFactory->createResourceItem($articleViewDocuments[2])->willReturn($articleResourceItems[2]);
+
+ $request = $this->prophesize(Request::class);
+ $request->get('object')->willReturn($articleDocuments[0]);
+ $request->getLocale()->willReturn('de');
+
+ $requestStack = $this->prophesize(RequestStack::class);
+ $requestStack->getCurrentRequest()->wilLReturn($request);
+
+ $referenceStore = $this->prophesize(ReferenceStore::class);
+ $referenceStore->add($articleDocuments[1]->getUuid())->shouldBeCalled();
+ $referenceStore->add($articleDocuments[2]->getUuid())->shouldBeCalled();
+
+ $extension = new ArticleViewDocumentTwigExtension(
+ $articleViewDocumentRepository->reveal(),
+ $articleResourceItemFactory->reveal(),
+ $referenceStore->reveal(),
+ $metadataFactory->reveal(),
+ $requestStack->reveal()
+ );
+
+ $this->assertEquals(
+ [
+ $articleResourceItems[1],
+ $articleResourceItems[2],
+ ],
+ $extension->loadRecent()
+ );
+ }
+
+ /**
+ * @return MetadataFactory
+ */
+ private function getMetadataFactory()
+ {
+ $metadata = new StructureMetadata();
+ $metadata->tags[] = ['name' => 'sulu_article.type', 'attributes' => ['type' => 'blog']];
+
+ $metadataFactory = $this->prophesize(StructureMetadataFactory::class);
+ $metadataFactory->getStructureMetadata('article', 'blog')->willReturn($metadata);
+
+ return $metadataFactory;
+ }
+
+ /**
+ * @return array
+ */
+ private function getArticleDocuments()
+ {
+ $ids = ['123-123-123', '321-321-321', '111-111-111'];
+
+ return array_map(
+ function ($id) {
+ $articleDocument = new ArticleDocument();
+ $articleDocument->setUuid($id);
+ $articleDocument->setLocale('de');
+ $articleDocument->setStructureType('blog');
+
+ return $articleDocument;
+ },
+ array_values($ids)
+ );
+ }
+
+ /**
+ * @param array $articleDocuments
+ *
+ * @return array
+ */
+ private function getArticleViewDocuments(array $articleDocuments)
+ {
+ return array_map(
+ function ($articleDocument) {
+ $articleViewDocument = new ArticleViewDocument($articleDocument->getUuid());
+ $articleViewDocument->setLocale($articleDocument->getLocale());
+ $articleViewDocument->setStructureType($articleDocument->getStructureType());
+
+ return $articleViewDocument;
+ },
+ array_values($articleDocuments)
+ );
+ }
+
+ /**
+ * @param array $articleDocuments
+ * @param array $articleViewDocuments
+ *
+ * @return array
+ */
+ private function getArticleResourceItems(array $articleDocuments, array $articleViewDocuments)
+ {
+ $articleResourceItems = [];
+
+ foreach ($articleViewDocuments as $key => $value) {
+ $articleResourceItems[] = new ArticleResourceItem($articleViewDocuments[$key], $articleDocuments[$key]);
+ }
+
+ return $articleResourceItems;
+ }
+
+ /**
+ * @param array $articleViewDocuments
+ *
+ * @return DocumentIterator
+ */
+ private function getDocumentIterator(array $articleViewDocuments)
+ {
+ $documentIteratorCount = 1;
+
+ $documentIterator = $this->prophesize(DocumentIterator::class);
+ $documentIterator->rewind()->willReturn(0);
+ $documentIterator->next()->willReturn($documentIteratorCount);
+ $documentIterator->current()->will(function() use (&$documentIteratorCount, $articleViewDocuments) {
+ return $articleViewDocuments[$documentIteratorCount++];
+ });
+ $documentIterator->valid()->will(function() use (&$documentIteratorCount, $articleViewDocuments) {
+ if (array_key_exists($documentIteratorCount, $articleViewDocuments)) {
+ return true;
+ }
+
+ return false;
+ });
+
+ return $documentIterator;
+ }
+}
diff --git a/Twig/ArticleViewDocumentTwigExtension.php b/Twig/ArticleViewDocumentTwigExtension.php
new file mode 100644
index 000000000..206a16871
--- /dev/null
+++ b/Twig/ArticleViewDocumentTwigExtension.php
@@ -0,0 +1,212 @@
+articleViewDocumentRepository = $articleViewDocumentRepository;
+ $this->articleResourceItemFactory = $articleResourceItemFactory;
+ $this->referenceStore = $referenceStore;
+ $this->structureMetadataFactory = $structureMetadataFactory;
+ $this->requestStack = $requestStack;
+ }
+
+ /**
+ * Returns an array of possible function in this extension.
+ *
+ * @return array
+ */
+ public function getFunctions()
+ {
+ return [
+ new \Twig_SimpleFunction('sulu_article_load_recent', [$this, 'loadRecent']),
+ new \Twig_SimpleFunction('sulu_article_load_similar', [$this, 'loadSimilar']),
+ ];
+ }
+
+ /**
+ * Loads recent articles with given parameters.
+ *
+ * @param int $limit
+ * @param null|array $types
+ * @param null|string $locale
+ *
+ * @return ArticleResourceItem[]
+ */
+ public function loadRecent(
+ $limit = ArticleViewDocumentRepository::DEFAULT_LIMIT,
+ array $types = null,
+ $locale = null
+ ) {
+ $excludeUuid = null;
+
+ /** @var Request $request */
+ $request = $this->requestStack->getCurrentRequest();
+
+ $articleDocument = $request->get('object');
+ if ($articleDocument instanceof ArticleDocument) {
+ $excludeUuid = $articleDocument->getUuid();
+
+ if (!$types) {
+ $types = [$this->getArticleType($articleDocument)];
+ }
+ }
+
+ if (!$locale) {
+ $locale = $request->getLocale();
+ }
+
+ $articleViewDocuments = $this->articleViewDocumentRepository->findRecent($excludeUuid, $limit, $types, $locale);
+
+ return $this->getResourceItems($articleViewDocuments);
+ }
+
+ /**
+ * Loads similar articles with given parameters.
+ *
+ * @param int $limit
+ * @param array|null $types
+ * @param null $locale
+ *
+ * @throws ArticleInRequestNotFoundException
+ *
+ * @return ArticleResourceItem[]
+ */
+ public function loadSimilar(
+ $limit = ArticleViewDocumentRepository::DEFAULT_LIMIT,
+ array $types = null,
+ $locale = null
+ ) {
+ $uuid = null;
+
+ $request = $this->requestStack->getCurrentRequest();
+
+ $articleDocument = $request->get('object');
+ if ($articleDocument instanceof ArticleDocument) {
+ $uuid = $articleDocument->getUuid();
+
+ if (!$types) {
+ $types = [$this->getArticleType($articleDocument)];
+ }
+ }
+
+ if (!$uuid) {
+ throw new ArticleInRequestNotFoundException();
+ }
+
+ if (!$locale) {
+ $locale = $request->getLocale();
+ }
+
+ $articleViewDocuments = $this->articleViewDocumentRepository->findSimilar($uuid, $limit, $types, $locale);
+
+ return $this->getResourceItems($articleViewDocuments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'sulu_article.article_view_document';
+ }
+
+ /**
+ * @param DocumentIterator $articleViewDocuments
+ *
+ * @return ArticleResourceItem[]
+ */
+ private function getResourceItems(DocumentIterator $articleViewDocuments)
+ {
+ $articleResourceItems = [];
+
+ /** @var ArticleViewDocument $articleViewDocument */
+ foreach ($articleViewDocuments as $articleViewDocument) {
+ $this->referenceStore->add($articleViewDocument->getUuid());
+ $articleResourceItems[] = $this->articleResourceItemFactory->createResourceItem($articleViewDocument);
+ }
+
+ return $articleResourceItems;
+ }
+
+ /**
+ * @param ArticleDocument $articleDocument
+ *
+ * @return string
+ */
+ private function getArticleType(ArticleDocument $articleDocument)
+ {
+ $structureMetadata = $this->structureMetadataFactory->getStructureMetadata(
+ 'article',
+ $articleDocument->getStructureType()
+ );
+
+ return $this->getType($structureMetadata);
+ }
+}