From f251b94c99b2498855c02f78de301b30c3ab17ea Mon Sep 17 00:00:00 2001 From: ONGR Team Date: Thu, 30 Oct 2014 13:59:42 +0100 Subject: [PATCH] Initial code dump --- .gitignore | 7 + .travis.yml | 12 + Controller/ContentController.php | 53 +++ DependencyInjection/Configuration.php | 58 +++ DependencyInjection/ONGRContentExtension.php | 52 +++ Document/CategoryTrait.php | 224 +++++++++++ LICENSE | 19 + ONGRContentBundle.php | 21 + README.md | 4 + Resources/config/routing.yml | 5 + Resources/config/services.yml | 43 ++ .../views/Content/plain_cms_snippet.html.twig | 1 + Service/CategoryList.php | 88 ++++ Service/CategoryService.php | 375 ++++++++++++++++++ Service/ContentService.php | 85 ++++ .../ONGRContentExtensionTest.php | 80 ++++ .../Functional/Service/ContentServiceTest.php | 30 ++ Tests/Functional/ServiceCreationTest.php | 53 +++ .../Twig/ContentExtensionsIntegrationTest.php | 102 +++++ Tests/Unit/Service/CategoryServiceTest.php | 344 ++++++++++++++++ Tests/Unit/Twig/CategoryExtensionTest.php | 185 +++++++++ Tests/Unit/Twig/ContentExtensionTest.php | 195 +++++++++ Tests/app/AppKernel.php | 49 +++ Tests/app/Resources/views/base.html.twig | 16 + Tests/app/config/config_test.yml | 57 +++ Tests/app/config/routing.yml | 3 + .../Acme/TestBundle/AcmeTestBundle.php | 21 + .../DependencyInjection/AcmeTestExtension.php | 38 ++ .../DependencyInjection/Configuration.php | 35 ++ .../Acme/TestBundle/Document/Category.php | 37 ++ .../Acme/TestBundle/Document/Content.php | 122 ++++++ .../Acme/TestBundle/Document/Product.php | 26 ++ .../TestBundle/Resources/config/services.yml | 4 + Twig/CategoryExtension.php | 233 +++++++++++ Twig/ContentExtension.php | 168 ++++++++ composer.json | 29 ++ phpunit.xml | 45 +++ 37 files changed, 2919 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Controller/ContentController.php create mode 100755 DependencyInjection/Configuration.php create mode 100644 DependencyInjection/ONGRContentExtension.php create mode 100755 Document/CategoryTrait.php create mode 100644 LICENSE create mode 100644 ONGRContentBundle.php create mode 100644 README.md create mode 100644 Resources/config/routing.yml create mode 100755 Resources/config/services.yml create mode 100644 Resources/views/Content/plain_cms_snippet.html.twig create mode 100644 Service/CategoryList.php create mode 100755 Service/CategoryService.php create mode 100644 Service/ContentService.php create mode 100644 Tests/Functional/DependecyInjection/ONGRContentExtensionTest.php create mode 100644 Tests/Functional/Service/ContentServiceTest.php create mode 100644 Tests/Functional/ServiceCreationTest.php create mode 100644 Tests/Functional/Twig/ContentExtensionsIntegrationTest.php create mode 100755 Tests/Unit/Service/CategoryServiceTest.php create mode 100755 Tests/Unit/Twig/CategoryExtensionTest.php create mode 100644 Tests/Unit/Twig/ContentExtensionTest.php create mode 100644 Tests/app/AppKernel.php create mode 100644 Tests/app/Resources/views/base.html.twig create mode 100644 Tests/app/config/config_test.yml create mode 100644 Tests/app/config/routing.yml create mode 100644 Tests/app/fixture/Acme/TestBundle/AcmeTestBundle.php create mode 100644 Tests/app/fixture/Acme/TestBundle/DependencyInjection/AcmeTestExtension.php create mode 100644 Tests/app/fixture/Acme/TestBundle/DependencyInjection/Configuration.php create mode 100644 Tests/app/fixture/Acme/TestBundle/Document/Category.php create mode 100644 Tests/app/fixture/Acme/TestBundle/Document/Content.php create mode 100644 Tests/app/fixture/Acme/TestBundle/Document/Product.php create mode 100644 Tests/app/fixture/Acme/TestBundle/Resources/config/services.yml create mode 100755 Twig/CategoryExtension.php create mode 100755 Twig/ContentExtension.php create mode 100644 composer.json create mode 100644 phpunit.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fa4850 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea/ +.DS_Store +/vendor/ +composer.lock +/Tests/app/cache/ +/Tests/app/logs/ +/Tests/app/build/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7a462a0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php +php: + - 5.4 + - 5.5 + - 5.6 +services: + - elasticsearch +before_script: + - composer update --prefer-dist +script: + - vendor/bin/phpunit + - vendor/bin/phpcs -p --standard=PSR2 --ignore=vendor/,Tests/app/ ./ diff --git a/Controller/ContentController.php b/Controller/ContentController.php new file mode 100644 index 0000000..77bf223 --- /dev/null +++ b/Controller/ContentController.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Response; + +/** + * Controller for content pages. + * + * @SuppressWarnings(UnusedFormalParameter) + */ +class ContentController extends Controller +{ + /** + * Returns template data for snippetAction. + * + * @param string $slug + * + * @return array + */ + protected function snippetActionData($slug) + { + return $this->get('ongr_content.content_service')->getDataForSnippet($slug); + } + + /** + * Render cms body in template. + * + * @param string $slug + * @param string $template + * + * @return Response + */ + public function snippetAction( + $slug, + $template = 'ONGRContentBundle:Content:plain_cms_snippet.html.twig' + ) { + return $this->render( + $template, + $this->snippetActionData($slug) + ); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100755 index 0000000..ab10a64 --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * This is the class that validates and merges configuration from app/config files. + */ +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('ongr_content'); + + $rootNode + ->children() + ->arrayNode('es') + ->isRequired() + ->children() + ->arrayNode('repositories') + ->children() + ->scalarNode('product')->isRequired()->end() + ->scalarNode('content')->isRequired()->end() + ->scalarNode('category')->isRequired()->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('snippet') + ->cannotBeOverwritten() + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('render_strategy') + ->defaultValue('esi') + ->info('Default template render strategy') + ->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/DependencyInjection/ONGRContentExtension.php b/DependencyInjection/ONGRContentExtension.php new file mode 100644 index 0000000..6e1f299 --- /dev/null +++ b/DependencyInjection/ONGRContentExtension.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; + +/** + * This is the class that loads and manages bundle configuration. + */ +class ONGRContentExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yml'); + + // Inject manager and repository to services. + $repositories = $config['es']['repositories']; + + $container->setParameter('ongr_content.es.repositories', $repositories); + + $contentService = $container->getDefinition('ongr_content.content_service'); + $contentService->addArgument(new Reference($repositories['content'])); + + $twigExtension = $container->getDefinition('ongr_content.twig.content_extension'); + $twigExtension->addArgument(new Reference($repositories['content'])); + + $categoryService = $container->getDefinition('ongr_content.category_service'); + $categoryService->addArgument(new Reference($repositories['category'])); + + $container->setParameter('ongr_content.snippet.render_strategy', $config['snippet']['render_strategy']); + } +} diff --git a/Document/CategoryTrait.php b/Document/CategoryTrait.php new file mode 100755 index 0000000..dbede0d --- /dev/null +++ b/Document/CategoryTrait.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Document; + +trait CategoryTrait +{ + /** + * @var string + * + * @ES\Property(type="string", name="root_id") + */ + public $rootId; + + /** + * @var int + * + * @ES\Property(type="integer", name="left") + */ + public $left; + + /** + * @var int + * + * @ES\Property(type="integer", name="right") + */ + public $right; + + /** + * @var string + * + * @ES\Property(type="string", name="sort") + */ + public $sort; + + /** + * @var bool + * + * @ES\Property(type="boolean", name="active") + */ + public $active; + + /** + * @var bool + * + * @ES\Property(type="boolean", name="hidden") + */ + public $hidden; + + /** + * @var string + * + * @ES\Property(type="string", name="parent_id") + */ + public $parentId; + + /** + * @var int + * + * @ES\Property(type="integer", name="level") + */ + private $level; + + /** + * @var bool + */ + private $expanded; + + /** + * @var bool + */ + private $current; + + /** + * @var array + */ + public $breadcrumbs; + + /** + * @var array + */ + private $children; + + /** + * Tests if category has any children. + * + * @return bool + */ + public function hasChildren() + { + return is_array($this->children) && count($this->children); + } + + /** + * Tests if category has any visible children. + * + * @return bool + */ + public function hasVisibleChildren() + { + if (is_array($this->children) && count($this->children)) { + foreach ($this->children as $child) { + if (!$child->hidden) { + return true; + } + } + } + + return false; + } + + /** + * @param array $children + */ + public function setChildren($children) + { + $this->children = $children; + } + + /** + * @return array + */ + public function getChildren() + { + return $this->children; + } + + /** + * @param string $key + * + * @return mixed + */ + public function getChild($key) + { + return $this->children[$key]; + } + + /** + * If key is null value is put to the end. + * + * @param string $key + * @param mixed $value + */ + public function setChild($key, $value) + { + if (empty($key)) { + $this->children[] = $value; + } else { + $this->children[$key] = $value; + } + } + + /** + * @param bool $current + */ + public function setCurrent($current) + { + $this->current = $current; + } + + /** + * @return bool + */ + public function getCurrent() + { + return $this->current; + } + + /** + * @param bool $expanded + */ + public function setExpanded($expanded) + { + $this->expanded = $expanded; + } + + /** + * @return bool + */ + public function getExpanded() + { + return $this->expanded; + } + + /** + * @param int $level + */ + public function setLevel($level) + { + $this->level = $level; + } + + /** + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * @param string $parentId + */ + public function setParentId($parentId) + { + $this->parentId = $parentId; + } + + /** + * @return string + */ + public function getGetParentId() + { + return $this->level; + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..95f1eff --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 NFQ Technologies UAB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/ONGRContentBundle.php b/ONGRContentBundle.php new file mode 100644 index 0000000..aed366e --- /dev/null +++ b/ONGRContentBundle.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * This class is used to register component in app kernel. + */ +class ONGRContentBundle extends Bundle +{ +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee7bb35 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +ONGR Content bundle +=========== + +Documentation will be soon.. diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml new file mode 100644 index 0000000..4768b2d --- /dev/null +++ b/Resources/config/routing.yml @@ -0,0 +1,5 @@ +_ongr_plain_cms_snippet: + pattern: /_proxy/cmsSnippet/{slug}/{template} + defaults: + _controller: ONGRContentBundle:Content:snippet + template: ONGRContentBundle:Content:plain_cms_snippet.html.twig diff --git a/Resources/config/services.yml b/Resources/config/services.yml new file mode 100755 index 0000000..cfbf172 --- /dev/null +++ b/Resources/config/services.yml @@ -0,0 +1,43 @@ +parameters: + ongr_content.twig.content_extension.class: ONGR\ContentBundle\Twig\ContentExtension + ongr_content.twig.category_extension.class: ONGR\ContentBundle\Twig\CategoryExtension + ongr_content.content_service.class: ONGR\ContentBundle\Service\ContentService + ongr_content.category_service.class: ONGR\ContentBundle\Service\CategoryService + ongr_content.category_list.class: ONGR\ContentBundle\Service\CategoryList + + ongr_content.product_per_page: 10 + +services: + + twig.extension.stringloader: + class: Twig_Extension_StringLoader + tags: + - { name: twig.extension } + ongr_content.twig.content_extension: + class: %ongr_content.twig.content_extension.class% + tags: + - { name: twig.extension } + arguments: + - @fragment.handler + - @router + - %ongr_content.snippet.render_strategy% + + ongr_content.content_service: + class: %ongr_content.content_service.class% + calls: + - [setLogger, [@?logger]] + + ongr_content.category_list: + class: %ongr_content.category_list.class% + calls: + - [setProductsPerPage, [%ongr_content.product_per_page%]] + + ongr_content.category_service: + class: %ongr_content.category_service.class% + + ongr_content.twig.category_extension: + class: %ongr_content.twig.category_extension.class% + tags: + - { name: twig.extension } + arguments: + - @ongr_content.category_service diff --git a/Resources/views/Content/plain_cms_snippet.html.twig b/Resources/views/Content/plain_cms_snippet.html.twig new file mode 100644 index 0000000..a224218 --- /dev/null +++ b/Resources/views/Content/plain_cms_snippet.html.twig @@ -0,0 +1 @@ +{{ include(template_from_string(content)) }} diff --git a/Service/CategoryList.php b/Service/CategoryList.php new file mode 100644 index 0000000..0e61ad0 --- /dev/null +++ b/Service/CategoryList.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Service; + +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\DSL\Query\TermQuery; +use ONGR\ElasticsearchBundle\DSL\Search; +use ONGR\ElasticsearchBundle\ORM\Repository; +use Symfony\Component\HttpFoundation\Request; + +/** + * Collects categories and products data. + */ +class CategoryList +{ + /** + * @var Repository + */ + protected $repository; + + /** + * @var int + */ + protected $productsPerPage; + + /** + * @param Request $request + * @param DocumentInterface $document + * + * @return array + */ + public function getCategoryData(Request $request, $document) + { + $page = max((int)$request->query->get('page'), 1); + $sort = $request->query->get('sort'); + $reverse = (bool)$request->query->get('desc'); + + $search = new Search(); + $search->addQuery(new TermQuery('categories', $document->id), 'must'); + + $urlParameters = [ + 'document' => $document, + 'sort' => $sort, + 'page' => $page, + ]; + + if ($reverse) { + $urlParameters['desc'] = 1; + } + + return [ + 'category' => $document, + 'page' => $page, + 'urlParameters' => $urlParameters, + 'urlRoute' => $request->get('_route'), + 'pager' => null, + 'products' => [], + 'count' => 0, + 'js' => $request->isXmlHttpRequest(), + 'selectedCategory' => $document->id, + ]; + } + + /** + * @param string $repositoryName + */ + public function setRepository($repositoryName) + { + $this->repository = $this->manager->getRepository($repositoryName); + } + + /** + * @param int $productsPerPage + */ + public function setProductsPerPage($productsPerPage) + { + $this->productsPerPage = $productsPerPage; + } +} diff --git a/Service/CategoryService.php b/Service/CategoryService.php new file mode 100755 index 0000000..490b568 --- /dev/null +++ b/Service/CategoryService.php @@ -0,0 +1,375 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Service; + +use ONGR\ContentBundle\Document\CategoryTrait; +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\DSL\Query\TermQuery; +use ONGR\ElasticsearchBundle\DSL\Search; +use ONGR\ElasticsearchBundle\DSL\Sort\Sort; +use ONGR\ElasticsearchBundle\ORM\Repository; +use ONGR\ElasticsearchBundle\Result\DocumentIterator; + +/** + * Used to manipulate category trees and nodes. + */ +class CategoryService +{ + /** + * @var Repository + */ + private $repository; + + /** + * @var array + */ + private $treeCache; + + /** + * @var bool + */ + private $loadHiddenCategories; + + /** + * @var DocumentInterface + */ + private $currentLeaf = null; + + /** + * @var null|string + */ + private $currentCategoryId = null; + + /** + * @var int + */ + private $limit = 1000; + + /** + * @param Repository $repository + */ + public function __construct(Repository $repository) + { + $this->repository = $repository; + } + + /** + * Builds a search query. + * + * @return Search + */ + protected function buildQuery() + { + /** @var Search $search */ + $search = $this->repository->createSearch(); + $search->setSize($this->limit); + $search->addSort(new Sort('left', Sort::ORDER_ASC)); + $search->addQuery(new TermQuery('active', true), 'must'); + if (!$this->loadHiddenCategories) { + $search->addQuery(new TermQuery('hidden', 0), 'must'); + } + + return $search; + } + + /** + * Builds a child node. + * + * @param CategoryTrait $node + * @param \ArrayIterator $references + * @param int $maxLevel + */ + private function buildChildNode($node, $references, $maxLevel) + { + if (isset($references[$node->parentId])) { + $level = $references[$node->parentId]->getLevel() + 1; + + // Check if max level is not reached or not set at all. + if ($maxLevel == 0 || $level <= $maxLevel) { + $node->setLevel($level); + $node->setParent($references[$node->parentId]); + $references[$node->parentId]->setChild($node->id, $node); + } + } + } + + /** + * Gets category by id. + * + * @param string $id Category ID. + * + * @return DocumentInterface|null + */ + public function getCategory($id) + { + $category = $this->repository->find($id); + + return $category; + } + + /** + * Builds a root node. + * + * @param CategoryTrait $node + * @param \ArrayIterator $tree + * @param int $level + */ + private function buildRootNode($node, $tree, $level) + { + $node->setLevel($level); + $tree[$node->id] = $node; + } + + /** + * Builds a node. Sets node parameters. + * + * @param CategoryTrait $node + * @param \ArrayIterator $references + * @param \ArrayIterator $tree + * @param int $maxLevel + */ + private function buildNode($node, $references, $tree, $maxLevel) + { + if ($node->id == $this->getCurrentCategoryId()) { + $node->setCurrent(true); + $this->currentLeaf = $node; + } + + $references[$node->id] = $node; + + if ($node->parentId == 'oxrootid') { + $this->buildRootNode($node, $tree, 1); + } else { + $this->buildChildNode($node, $references, $maxLevel); + } + } + + /** + * Expands nodes. + * + * @param \ArrayIterator $references + */ + private function expandNodes($references) + { + $id = $this->getCurrentCategoryId(); + + if ($id) { + while (isset($references[$id])) { + $references[$id]->setExpanded(true); + $id = $references[$id]->parentId; + } + } + } + + /** + * Builds nested tree from single category list. + * + * @param array $dataSet A set of nodes. + * @param int $maxLevel Maximum levels deep of the tree to build. + * + * @return array + */ + private function buildTree($dataSet, $maxLevel = 0) + { + $tree = new \ArrayIterator(); + $references = new \ArrayIterator(); + + /** @var DocumentIterator $dataSet */ + foreach ($dataSet as $node) { + if ($node->active) { + $this->buildNode($node, $references, $tree, $maxLevel); + } + } + + $this->expandNodes($references); + + $this->sortChildTree($tree); + + return $tree; + } + + /** + * Sorts category tree by sort field. + * + * @param array|\ArrayIterator $tree + */ + protected function sortChildTree(&$tree) + { + /** @var CategoryTrait $node */ + if (is_array($tree)) { + uasort($tree, array($this, 'sortNodes')); + foreach ($tree as $node) { + $children = $node->getChildren(); + if ($children) { + $this->sortChildTree($children); + $node->setChildren($children); + } + } + } else { + $tree->uasort(array($this, 'sortNodes')); + $tree->rewind(); + foreach ($tree as $node) { + $children = $node->getChildren(); + if ($children) { + $this->sortChildTree($children); + $node->setChildren($children); + } + } + } + } + + /** + * Sorts nodes by field sort if value equal then by field left. + * + * @param CategoryTrait $a + * @param CategoryTrait $b + * + * @return int + */ + public function sortNodes($a, $b) + { + if ($a->sort < $b->sort) { + return -1; + } elseif ($a->sort > $b->sort) { + return 1; + } elseif ($a->left < $b->left) { + return -1; + } elseif ($a->left > $b->left) { + return 1; + } + + return 0; + } + + /** + * Returns nested category tree. + * + * @param int $maxLevel + * @param bool $getHidden + * + * @return \ArrayIterator + */ + public function getTree($maxLevel = 0, $getHidden = false) + { + $hash = $maxLevel . $getHidden; + if (!isset($this->treeCache[$hash])) { + $this->setLoadHiddenCategories($getHidden); + $query = $this->buildQuery(); + $results = $this->repository->execute($query, Repository::RESULTS_OBJECT); + $tree = $this->buildTree($results, $maxLevel); + $this->treeCache[$hash] = $tree; + } + + return $this->treeCache[$hash]; + } + + /** + * @param int $maxLevel + * @param string $categoryId + * + * @return array + * @throws \ErrorException + */ + public function getPartialTree($maxLevel = 0, $categoryId = null) + { + if ($categoryId === null) { + throw new \ErrorException('Category Id must be defined on getPartialTree() method'); + } + $tree = $this->getTree($maxLevel, true); + $tree->rewind(); + $partialTree = $this->findPartialTree($tree, $categoryId); + + return $partialTree; + } + + /** + * Returns partial tree by category ID. + * + * @param array $tree + * @param string $categoryId + * + * @return array + */ + protected function findPartialTree($tree, $categoryId) + { + /** @var CategoryTrait $node */ + foreach ($tree as $node) { + if ($node->id == $categoryId) { + return array($node); + } + if ($node->getChildren()) { + $result = $this->findPartialTree($node->getChildren(), $categoryId); + if (!empty($result)) { + return $result; + } + } + } + + return []; + } + + /** + * @param Repository $repository + */ + public function setRepository($repository) + { + $this->repository = $repository; + } + + /** + * @param int $limit + */ + public function setLimit($limit) + { + $this->limit = $limit; + } + + /** + * Gets current category ID. + * + * @return mixed + */ + public function getCurrentCategoryId() + { + return $this->currentCategoryId; + } + + /** + * Temporary function for forcing categoryId parameter. + * + * @param string $categoryId + */ + public function setCurrentCategoryId($categoryId) + { + $this->currentCategoryId = $categoryId; + } + + /** + * Gets current category document. + * + * @return mixed + */ + public function getCurrentCategoryDocument() + { + return $this->currentLeaf; + } + + /** + * Set if load all categories or hidden. + * + * @param bool $param + */ + protected function setLoadHiddenCategories($param) + { + $this->loadHiddenCategories = $param; + } +} diff --git a/Service/ContentService.php b/Service/ContentService.php new file mode 100644 index 0000000..f561c07 --- /dev/null +++ b/Service/ContentService.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Service; + +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\DSL\Query\TermQuery; +use ONGR\ElasticsearchBundle\ORM\Repository; +use Psr\Log\LoggerAwareTrait; + +/** + * Content management service. + */ +class ContentService +{ + use LoggerAwareTrait; + + /** + * @var Repository + */ + protected $repository; + + /** + * @param Repository $repository + */ + public function __construct($repository) + { + $this->repository = $repository; + } + + /** + * Retrieves document by slug. + * + * @param string $slug + * + * @return DocumentInterface|null + */ + public function getDocumentBySlug($slug) + { + $search = $this->repository->createSearch(); + $search->addQuery(new TermQuery('slug', $slug), 'must'); + + $results = $this->repository->execute($search); + + if (isset($results)) { + return $results->current(); + } + + return null; + } + + /** + * Returns data for content snippet render action. + * + * @param string $slug + * + * @return array + */ + public function getDataForSnippet($slug) + { + $document = $this->getDocumentBySlug($slug); + + if (!$document) { + $this->logger && $this->logger->warning( + sprintf("Can not render snippet for '%s' because content was not found.", $slug) + ); + + return ['content' => '', 'title' => null, 'document' => null]; + } + + return [ + 'content' => $document->content, + 'title' => $document->title, + 'document' => $document, + ]; + } +} diff --git a/Tests/Functional/DependecyInjection/ONGRContentExtensionTest.php b/Tests/Functional/DependecyInjection/ONGRContentExtensionTest.php new file mode 100644 index 0000000..6039c8a --- /dev/null +++ b/Tests/Functional/DependecyInjection/ONGRContentExtensionTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Functional\DependecyInjection; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\DependencyInjection\Exception\LogicException; + +class ONGRContentExtensionTest extends WebTestCase +{ + /** + * @return array + */ + public function getParametersData() + { + $out = []; + + $out[] = ['ongr_content.snippet.render_strategy', 'string', true, 'newValue']; + + return $out; + } + + /** + * Tests if parameters are being set. + * + * @param string $name + * @param string $type + * + * @dataProvider getParametersData + */ + public function testParameters($name, $type) + { + $container = self::createClient()->getContainer(); + + $this->assertTrue($container->hasParameter($name), "Parameter '{$name}' is not set"); + + switch ($type) { + case 'int': + $this->assertTrue( + is_int($container->getParameter($name)), + "Parameter {$name} should be integer." + ); + break; + default: + $this->assertTrue( + is_string($container->getParameter($name)), + "Parameter {$name} should be string by default." + ); + break; + } + } + + /** + * Tests if exception is being thrown by overwriting values. + * + * @param string $name + * @param bool $overwrite + * @param mixed $overwriteValue + * + * @dataProvider getParametersData + * + * @expectedException LogicException + */ + public function testParametersOverwrite($name, $overwrite, $overwriteValue) + { + $container = self::createClient()->getContainer(); + + if ($overwrite && $container->hasParameter($name)) { + $container->setParameter($name, $overwriteValue); + } + } +} diff --git a/Tests/Functional/Service/ContentServiceTest.php b/Tests/Functional/Service/ContentServiceTest.php new file mode 100644 index 0000000..a3b0a47 --- /dev/null +++ b/Tests/Functional/Service/ContentServiceTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Functional\Service; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class ContentServiceTest extends WebTestCase +{ + /** + * Test if content service can be created. + */ + public function testGetContentService() + { + $container = self::createClient()->getContainer(); + $this->assertTrue($container->has('ongr_content.content_service')); + $this->assertInstanceOf( + 'ONGR\\ContentBundle\\Service\\ContentService', + $container->get('ongr_content.content_service') + ); + } +} diff --git a/Tests/Functional/ServiceCreationTest.php b/Tests/Functional/ServiceCreationTest.php new file mode 100644 index 0000000..2593439 --- /dev/null +++ b/Tests/Functional/ServiceCreationTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +/** + * Checks if services are created correctly. + */ +class ServiceCreationTest extends WebTestCase +{ + /** + * Tests if service are created correctly. + * + * @param string $service + * @param string $instance + * + * @dataProvider getTestServiceCreateData() + */ + public function testServiceCreate($service, $instance) + { + $container = self::createClient()->getKernel()->getContainer(); + + $this->assertTrue($container->has($service)); + $service = $container->get($service); + + $this->assertInstanceOf($instance, $service); + } + + /** + * Data provider for testServiceCreate(). + * + * @return array[] + */ + public function getTestServiceCreateData() + { + return [ + [ + 'ongr_content.category_service', + 'ONGR\\ContentBundle\\Service\\CategoryService', + ], + ]; + } +} diff --git a/Tests/Functional/Twig/ContentExtensionsIntegrationTest.php b/Tests/Functional/Twig/ContentExtensionsIntegrationTest.php new file mode 100644 index 0000000..b4c4402 --- /dev/null +++ b/Tests/Functional/Twig/ContentExtensionsIntegrationTest.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Functional\Twig; + +use ONGR\ContentBundle\Twig\ContentExtension; +use ONGR\ElasticsearchBundle\Test\ElasticsearchTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; + +class ContentExtensionsIntegrationTest extends ElasticsearchTestCase +{ + /** + * {@inheritdoc} + */ + protected function getDataArray() + { + return [ + 'default' => [ + 'content' => [ + [ + '_id' => 'testContent', + 'slug' => 'testContentSlug', + 'title' => 'Title', + 'content' => 'Content of test page', + 'short_description' => 'Test short description', + ], + ], + ], + ]; + } + + /** + * Test data provider for testSnippetFunctionIntegration(). + * + * @return array + */ + public function getTestSnippetFunctionIntegrationData() + { + $out = array(); + + // Case #0: empty request. + $request = new Request(); + $slug = 'testContentSlug'; + $expectedContent = 'Content of test page'; + + $out[] = [$request, $slug, $expectedContent]; + + // Case #1: esi request. + $request = new Request(); + $request->headers->add( + [ + 'Surrogate_Capability' => 'ESI/1.0', + ] + ); + $slug = 'testContentSlug'; + + $expectedContent = 'getContainer()->has('request_stack')) { + /** @var RequestStack $requestStack */ + $requestStack = $this->getContainer()->get('request_stack'); + $requestStack->push($request); + } + + $handler = $this->getContainer()->get('fragment.handler'); + $handler->setRequest($request); + $router = $this->getContainer()->get('router'); + + $repository = $this->getManager()->getRepository('AcmeTestBundle:Content'); + + $extension = new ContentExtension($handler, $router, $strategy, $repository); + + $this->assertContains($expectedContent, $extension->snippetFunction($slug)); + } +} diff --git a/Tests/Unit/Service/CategoryServiceTest.php b/Tests/Unit/Service/CategoryServiceTest.php new file mode 100755 index 0000000..437ced7 --- /dev/null +++ b/Tests/Unit/Service/CategoryServiceTest.php @@ -0,0 +1,344 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Unit\Service; + +use ONGR\ContentBundle\Service\CategoryService; +use ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\Document\Category; +use ONGR\ElasticsearchBundle\Test\ElasticsearchTestCase; + +/** + * Provides tests for category service. + */ +class CategoryServiceTest extends ElasticsearchTestCase +{ + /** + * {@inheritdoc} + */ + protected function getDataArray() + { + return [ + 'default' => [ + 'category' => [ + [ + '_id' => 'cat1', + 'active' => true, + 'sort' => 1, + 'left' => 2, + 'parent_id' => 'oxrootid', + 'level' => 1, + 'hidden' => false, + ], + [ + '_id' => 'cat2', + 'active' => true, + 'sort' => 2, + 'left' => 8, + 'parent_id' => 'oxrootid', + 'level' => 1, + 'hidden' => false, + ], + ], + ], + ]; + } + + /** + * Mock helper. + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getTestDataService() + { + $repository = $this->getMockBuilder('ElasticsearchBundle\\ORM\\Repository') + ->disableOriginalConstructor() + ->getMock(); + + $manager = $this->getMockBuilder('ElasticsearchBundle\\ORM\\Manager') + ->disableOriginalConstructor() + ->getMock(); + $manager->expects($this->any()) + ->method('getRepository') + ->will($this->returnValue($repository)); + + return $manager; + } + + /** + * Setter and getter tests. + */ + public function testSetGetCurrentCategoryId() + { + $repository = $this->getManager()->getRepository('AcmeTestBundle:Category'); + + $service = new CategoryService($repository); + $service->setRepository($repository); + + $id = 'testid'; + + $service->setCurrentCategoryId($id); + $this->assertEquals($id, $service->getCurrentCategoryId()); + } + + /** + * Builds a category. + * + * @param array $category + * + * @return Category + */ + protected function buildCategory($category) + { + $cat = new Category(); + $cat->id = $category['id']; + $cat->active = $category['active']; + $cat->sort = $category['sort']; + $cat->left = $category['left']; + $cat->parentId = $category['parent_id']; + $cat->setLevel($category['level']); + isset($category['current']) && $cat->setCurrent($category['current']); + isset($category['expanded']) && $cat->setExpanded($category['expanded']); + + return $cat; + } + + /** + * Provides tree dummy data for multiple tests. + * + * @return array + */ + public function treeDataProvider() + { + $catData = [ + [ + 'id' => 'cat1', + 'active' => true, + 'sort' => 1, + 'left' => 2, + 'parent_id' => 'oxrootid', + 'level' => 1, + ], + [ + 'id' => 'cat2', + 'active' => true, + 'sort' => 2, + 'left' => 8, + 'parent_id' => 'oxrootid', + 'level' => 1, + ], + [ + 'id' => 'cat3', + 'active' => true, + 'sort' => 1, + 'left' => 1, + 'parent_id' => 'oxrootid', + 'level' => 1, + 'current' => true, + 'expanded' => true, + ], + [ + 'id' => 'cat4', + 'active' => true, + 'sort' => 1, + 'left' => 3, + 'parent_id' => 'oxrootid', + 'level' => 1, + ], + [ + 'id' => 'cat41', + 'active' => true, + 'sort' => 1, + 'left' => 4, + 'parent_id' => 'cat4', + 'level' => 2, + ], + [ + 'id' => 'cat42', + 'active' => true, + 'sort' => 1, + 'left' => 5, + 'parent_id' => 'cat4', + 'level' => 2, + ], + [ + 'id' => 'cat421', + 'active' => true, + 'sort' => 2, + 'left' => 7, + 'parent_id' => 'cat42', + 'level' => 3, + ], + [ + 'id' => 'cat422', + 'active' => true, + 'sort' => 1, + 'left' => 6, + 'parent_id' => 'cat42', + 'level' => 3, + ], + [ + 'id' => 'cat5', + 'active' => true, + 'sort' => 1, + 'left' => 3, + 'parent_id' => 'oxrootid', + 'level' => 1, + ], + [ + 'id' => 'cat6', + 'active' => true, + 'sort' => 1, + 'left' => 9, + 'parent_id' => 'oxrootid', + 'level' => 1, + ], + ]; + + $data = []; + foreach ($catData as $category) { + $data[$category['id']] = $this->buildCategory($category); + } + + return [['data' => $data]]; + } + + /** + * Data provider for testGetPartialTree(). + * + * @return array + */ + public function getPartialTreeDataProvider() + { + // Case #0. + $cat1 = $this->buildCategory( + [ + 'id' => 'cat1', + 'active' => true, + 'sort' => 1, + 'left' => 2, + 'parent_id' => 'oxrootid', + 'level' => 1, + ] + ); + + $out[] = [new \ArrayIterator([$cat1]), 5, 'cat1', [$cat1]]; + + // Case #1 test finding in deeper level with multiple side categories. + $cat2 = $this->buildCategory( + [ + 'id' => 'cat2', + 'active' => true, + 'sort' => 2, + 'left' => 8, + 'parent_id' => 'oxrootid', + 'level' => 1, + ] + ); + + $cat3 = $this->buildCategory( + [ + 'id' => 'cat3', + 'active' => true, + 'sort' => 1, + 'left' => 1, + 'parent_id' => 'oxrootid', + 'level' => 1, + 'current' => true, + 'expanded' => true, + ] + ); + + $cat4 = $this->buildCategory( + [ + 'id' => 'cat4', + 'active' => true, + 'sort' => 1, + 'left' => 3, + 'parent_id' => 'oxrootid', + 'level' => 1, + ] + ); + + $cat41 = $this->buildCategory( + [ + 'id' => 'cat41', + 'active' => true, + 'sort' => 1, + 'left' => 4, + 'parent_id' => 'cat4', + 'level' => 2, + ] + ); + + $cat42 = $this->buildCategory( + [ + 'id' => 'cat42', + 'active' => true, + 'sort' => 1, + 'left' => 5, + 'parent_id' => 'cat4', + 'level' => 2, + ] + ); + + $cat421 = $this->buildCategory( + [ + 'id' => 'cat421', + 'active' => true, + 'sort' => 2, + 'left' => 7, + 'parent_id' => 'cat42', + 'level' => 3, + ] + ); + + $tree = [$cat1, $cat2, $cat3, $cat4]; + + $cat4->setChild('cat42', $cat42); + $cat4->setChild('cat41', $cat41); + $cat42->setChild('cat421', $cat421); + + $out[] = [new \ArrayIterator($tree), 5, 'cat42', [$cat42]]; + + // Case #2 test with improper arguments. + $out[] = [[], 0, null, null, 'Category Id must be defined on getPartialTree() method']; + + return $out; + } + + /** + * Tests getPartialTree. + * + * @param \ArrayIterator $tree + * @param int $maxLevel + * @param int $categoryId + * @param \ArrayIterator $expectedTree + * @param string $exception + * + * @dataProvider getPartialTreeDataProvider + */ + public function testGetPartialTree($tree, $maxLevel, $categoryId, $expectedTree, $exception = '') + { + /** @var CategoryService|\PHPUnit_Framework_MockObject_MockObject $categoryService */ + $categoryService = $this->getMockBuilder('ONGR\ContentBundle\Service\CategoryService') + ->disableOriginalConstructor() + ->setMethods(['getTree']) + ->getMock(); + if (!empty($exception)) { + $this->setExpectedException('\ErrorException', $exception); + } else { + $categoryService->expects($this->once())->method('getTree')->with($maxLevel, true)->willReturn($tree); + } + + $actualTree = $categoryService->getPartialTree($maxLevel, $categoryId); + $this->assertEquals($expectedTree, $actualTree); + } +} diff --git a/Tests/Unit/Twig/CategoryExtensionTest.php b/Tests/Unit/Twig/CategoryExtensionTest.php new file mode 100755 index 0000000..48a4743 --- /dev/null +++ b/Tests/Unit/Twig/CategoryExtensionTest.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Unit\Twig; + +use ONGR\ContentBundle\Service\CategoryService; +use ONGR\ContentBundle\Twig\CategoryExtension; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class CategoryExtensionTest extends WebTestCase +{ + /** + * Test if name is set on extension. + */ + public function testGetName() + { + $extension = new CategoryExtension(null); + $this->assertEquals(CategoryExtension::NAME, $extension->getName()); + } + + /** + * Test if max level is set correctly. + */ + public function testSetGetMaxLevel() + { + $maxLevel = 10; + + $extension = new CategoryExtension(null); + $extension->setMaxLevel($maxLevel); + $this->assertEquals($maxLevel, $extension->getMaxLevel()); + } + + /** + * Test if template is set correctly. + */ + public function testSetGetTemplate() + { + $template = 'test template'; + + $extension = new CategoryExtension(null); + $extension->setTemplate($template); + $this->assertEquals($template, $extension->getTemplate()); + } + + /** + * Test if Twig extension returns proper functions. + */ + public function testGetFunctions() + { + $extension = new CategoryExtension(null); + + $expectedFunctionsNames = ['category_tree', 'render_tree', 'category_child_tree']; + $actualFunctionNames = array_keys($extension->getFunctions()); + + $this->assertEquals($expectedFunctionsNames, $actualFunctionNames); + } + + /** + * Tests renderCategoryTree(). + */ + public function testRenderCategoryTree() + { + $tree = [1]; + $template = 'testTemplate'; + $expected = 'testStr'; + + $environment = $this->getMock('Twig_Environment', array('render')); + $environment->expects($this->once()) + ->method('render') + ->with( + $template, + [ + 'categories' => $tree, + 'selected_category' => null, + 'current_category' => null, + ] + ) + ->will($this->returnValue($expected)); + + $extension = new CategoryExtension(null); + $extension->setTemplate($template); + + $this->assertEquals($expected, $extension->renderCategoryTree($environment, $tree)); + + $this->assertNull($extension->renderCategoryTree($environment, [])); + } + + /** + * Tests getCategoryTree(). + */ + public function testGetCategoryTree() + { + $maxLevel = 10; + $template = 'testTemplate'; + $expected = 'testStr'; + $tree = [1]; + $categoryDocument = new \stdClass(); + + $environment = $this->getMock('Twig_Environment', array('render')); + $environment->expects($this->once()) + ->method('render') + ->with( + $template, + [ + 'categories' => $tree, + 'selected_category' => null, + 'current_category' => $categoryDocument, + ] + ) + ->will($this->returnValue($expected)); + + $service = $this->getMock('stdClass', ['getTree', 'setCurrentCategoryId', 'getCurrentCategoryDocument']); + $service->expects($this->once())->method('getTree')->with($maxLevel)->will($this->returnValue($tree)); + $service->expects($this->once())->method('getCurrentCategoryDocument')->will( + $this->returnValue($categoryDocument) + ); + + $extension = new CategoryExtension($service); + + $this->assertEquals($expected, $extension->getCategoryTree($environment, $template, $maxLevel)); + } + + /** + * Tests getCategoryChildTree(). + */ + public function testGetCategoryChildTree() + { + $maxLevel = 10; + $template = 'testTemplate'; + $expected = 'testStr'; + $selectedCategory = 'selectedCategory'; + $fromCategory = 'fromCategory'; + $tree = [1]; + $categoryDocument = new \stdClass(); + + /** @var \Twig_Environment|\PHPUnit_Framework_MockObject_MockObject $environment */ + $environment = $this->getMock('Twig_Environment', ['render']); + $environment->expects($this->once()) + ->method('render') + ->with( + $template, + [ + 'categories' => $tree, + 'selected_category' => $selectedCategory, + 'current_category' => $categoryDocument, + ] + ) + ->will($this->returnValue($expected)); + + /** @var CategoryService|\PHPUnit_Framework_MockObject_MockObject $service */ + $service = $this->getMock('stdClass', ['getPartialTree', 'setCurrentCategoryId', 'getCurrentCategoryDocument']); + + $service->expects($this->once()) + ->method('getPartialTree') + ->with($maxLevel, $fromCategory) + ->will($this->returnValue($tree)); + + $service->expects($this->once()) + ->method('getCurrentCategoryDocument') + ->will( + $this->returnValue($categoryDocument) + ); + + $extension = new CategoryExtension($service); + + $this->assertEquals( + $expected, + $extension->getCategoryChildTree( + $environment, + $template, + $maxLevel, + $selectedCategory, + $fromCategory + ) + ); + } +} diff --git a/Tests/Unit/Twig/ContentExtensionTest.php b/Tests/Unit/Twig/ContentExtensionTest.php new file mode 100644 index 0000000..9592b30 --- /dev/null +++ b/Tests/Unit/Twig/ContentExtensionTest.php @@ -0,0 +1,195 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\Unit\Twig; + +use ONGR\ContentBundle\Twig\ContentExtension; +use ONGR\ElasticsearchBundle\ORM\Manager; + +/** + * Provides tests for content extension. + */ +class ContentExtensionTest extends \PHPUnit_Framework_TestCase +{ + + /** + * Test getName function. + */ + public function testGetName() + { + $extension = $this->getContentExtension([]); + $this->assertEquals($extension->getName(), 'content_extension'); + } + + /** + * Test getFunctions. + */ + public function testGetFunctions() + { + $extension = $this->getContentExtension([]); + + foreach ($extension->getFunctions() as $function => $object) { + switch ($function) { + case 'snippet': + case 'getContentsBySlugs': + $this->assertFalse($object->needsEnvironment()); + break; + default: + $this->assertTrue($object->needsEnvironment()); + break; + } + $this->assertTrue(method_exists($extension, $function . 'Function')); + } + } + + /** + * Function test. + */ + public function testGetContentsBySlugsFunction() + { + $document = new \stdClass(); + $document->slug = 1; + $document->title = 'testTitle'; + $document->content = 'testContent'; + + $document2 = new \stdClass(); + $document2->slug = 2; + $document2->title = 'testTitle2'; + $document2->content = 'testContent2'; + + $extension = $this->getContentExtension([$document2, $document]); + + $this->assertEquals(array($document, $document2), $extension->getContentsBySlugsFunction(array(1, 2), true)); + } + + /** + * Test data provider for testSnippetFunction(). + * + * @return array + */ + public function getTestSnippetFunctionData() + { + $out = array(); + + // Case #0: inline strategy. + $out[] = array('inline'); + + // Case #1: esi strategy. + $out[] = array('esi'); + + // Case #2: ssi strategy. + $out[] = array('ssi'); + + return $out; + } + + /** + * Tests whether extension is called with correct parameters. + * + * @param string $strategy + * + * @dataProvider getTestSnippetFunctionData() + */ + public function testSnippetFunction($strategy) + { + $router = $this->getRouterMock(); + + $fragmentHandlerMock = $this->getFragmentHandlerMock(); + $fragmentHandlerMock + ->expects($this->exactly(1)) + ->method('render') + ->with( + $this->anything(), + $strategy + ); + + $document = new \stdClass(); + $document->slug = 1; + $document->title = 'testTitle'; + $document->content = 'testContent'; + + $extension = new ContentExtension( + $fragmentHandlerMock, + $router, + $strategy, + $this->getManager([$document]), + 'AcmeTestBundle:Content' + ); + + $extension->snippetFunction(1); + } + + /** + * @param array $results + * + * @return ContentExtension + */ + protected function getContentExtension($results = []) + { + $extension = new ContentExtension( + $this->getFragmentHandlerMock(), + $this->getRouterMock(), + null, + $this->getManager($results)->getRepository('AcmeTestBundle:Content') + ); + + return $extension; + } + + /** + * Returns Manager mock. + * + * @param array $result + * + * @return \PHPUnit_Framework_MockObject_MockObject|Manager + */ + protected function getManager($result = []) + { + $manager = $this->getMockBuilder('ONGR\ElasticsearchBundle\ORM\Manager') + ->disableOriginalConstructor() + ->getMock(); + + $repository = $this->getMockBuilder('ONGR\ElasticsearchBundle\ORM\Repository') + ->disableOriginalConstructor() + ->setMethods(['execute']) + ->getMock(); + + $repository->expects($this->any()) + ->method('execute') + ->willReturn($result); + + $manager->expects($this->any()) + ->method('getRepository') + ->with('AcmeTestBundle:Content') + ->willReturn($repository); + + return $manager; + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getFragmentHandlerMock() + { + return $this->getMock( + 'Symfony\\Component\\HttpKernel\\Fragment\\FragmentHandler', + ['render'] + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getRouterMock() + { + return $this->getMock('Symfony\\Component\\Routing\\RouterInterface'); + } +} diff --git a/Tests/app/AppKernel.php b/Tests/app/AppKernel.php new file mode 100644 index 0000000..4b69e56 --- /dev/null +++ b/Tests/app/AppKernel.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\HttpKernel\Kernel; + +/** + * AppKernel for tests. + */ +class AppKernel extends Kernel +{ + /** + * Registering bundles. + * + * @return array + */ + public function registerBundles() + { + return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\TwigBundle\TwigBundle(), + new Symfony\Bundle\MonologBundle\MonologBundle(), + new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(), + new ONGR\RouterBundle\ONGRRouterBundle(), + new ONGR\ContentBundle\ONGRContentBundle(), + + // For testing document loading. + new \ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\AcmeTestBundle(), + ]; + } + + /** + * Container configuration. + * + * @param LoaderInterface $loader + */ + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml'); + } +} diff --git a/Tests/app/Resources/views/base.html.twig b/Tests/app/Resources/views/base.html.twig new file mode 100644 index 0000000..05517d8 --- /dev/null +++ b/Tests/app/Resources/views/base.html.twig @@ -0,0 +1,16 @@ + + + + + + {% spaceless %} + {% block title %} + {% endblock %} + {% endspaceless %} + + + + {% block content %} + {% endblock %} + + diff --git a/Tests/app/config/config_test.yml b/Tests/app/config/config_test.yml new file mode 100644 index 0000000..17ab7c3 --- /dev/null +++ b/Tests/app/config/config_test.yml @@ -0,0 +1,57 @@ +# Framework Configuration +framework: + esi: ~ + fragments: { path: /_proxy } + secret: "TOP-SECRET" + router: + resource: "%kernel.root_dir%/config/routing.yml" + strict_requirements: %kernel.debug% + templating: + engines: ['twig'] + test: ~ + +monolog: + handlers: + file: + type: stream + action_level: error + path: %kernel.logs_dir%/test.log +ongr_elasticsearch: + connections: + default: + hosts: + - { host: 127.0.0.1:9200 } + index_name: ongr-content-bundle-test + settings: + refresh_interval: -1 + number_of_replicas: 0 + managers: + default: + connection: default + mappings: + - AcmeTestBundle + +ongr_content: + es: + repositories: + product: es.manager.default.product + content: es.manager.default.content + category: es.manager.default.category +ongr_router: + es_manager: default + seo_routes: + product: + _route: ongr_product + _controller: ONGRDemoBundle:Product:document + _default_route: ongr_product + _id_param: document + category: + _route: ongr_category + _default_route: ongr_category + _id_param: document + _controller: ONGRDemoBundle:Category:document + content: + _route: ongr_content + _default_route: ongr_content + _id_param: document + _controller: ONGRDemoBundle:Content:document diff --git a/Tests/app/config/routing.yml b/Tests/app/config/routing.yml new file mode 100644 index 0000000..cad44d0 --- /dev/null +++ b/Tests/app/config/routing.yml @@ -0,0 +1,3 @@ +ongr_content: + resource: "@ONGRContentBundle/Resources/config/routing.yml" + prefix: / diff --git a/Tests/app/fixture/Acme/TestBundle/AcmeTestBundle.php b/Tests/app/fixture/Acme/TestBundle/AcmeTestBundle.php new file mode 100644 index 0000000..0e75b48 --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/AcmeTestBundle.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * Dummy AcmeTestBundle. + */ +class AcmeTestBundle extends Bundle +{ +} diff --git a/Tests/app/fixture/Acme/TestBundle/DependencyInjection/AcmeTestExtension.php b/Tests/app/fixture/Acme/TestBundle/DependencyInjection/AcmeTestExtension.php new file mode 100644 index 0000000..31c73c7 --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/DependencyInjection/AcmeTestExtension.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; + +/** + * This is the class that loads and manages your bundle configuration. + * + * To learn more see + * {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} + */ +class AcmeTestExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yml'); + } +} diff --git a/Tests/app/fixture/Acme/TestBundle/DependencyInjection/Configuration.php b/Tests/app/fixture/Acme/TestBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..209b545 --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/DependencyInjection/Configuration.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * This is the class that validates and merges configuration from your app/config files. + * + * To learn more see + * {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} + */ +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $treeBuilder->root('acme_test'); + + return $treeBuilder; + } +} diff --git a/Tests/app/fixture/Acme/TestBundle/Document/Category.php b/Tests/app/fixture/Acme/TestBundle/Document/Category.php new file mode 100644 index 0000000..e090ee7 --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/Document/Category.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\Document; + +use ONGR\ElasticsearchBundle\Annotation as ES; +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\Document\DocumentTrait; +use ONGR\ContentBundle\Document\CategoryTrait; +use ONGR\RouterBundle\Document\SeoAwareTrait; + +/** + * Dummy category document. + * + * @ES\Document(type="category") + */ +class Category implements DocumentInterface +{ + use DocumentTrait; + use CategoryTrait; + use SeoAwareTrait; + + /** + * @var string + * + * @ES\Property(type="string", name="title") + */ + public $title; +} diff --git a/Tests/app/fixture/Acme/TestBundle/Document/Content.php b/Tests/app/fixture/Acme/TestBundle/Document/Content.php new file mode 100644 index 0000000..7ab863d --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/Document/Content.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\Document; + +use ONGR\ElasticsearchBundle\Annotation as ES; +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\Document\DocumentTrait; +use ONGR\RouterBundle\Document\SeoAwareTrait; + +/** + * Dummy content document. + * + * @ES\Document(type="content") + */ +class Content implements DocumentInterface +{ + use DocumentTrait; + use SeoAwareTrait; + + /** + * @var string + * + * @ES\Property(type="string", name="slug", index="not_analyzed") + */ + public $slug; + + /** + * @var string + * + * @ES\Property(type="string", name="title", index="not_analyzed") + */ + public $title; + + /** + * @var string + * + * @ES\Property(type="string", name="short_description", index="not_analyzed") + */ + public $shortDescription; + + /** + * @var int + * + * @ES\Property(type="integer", name="left") + */ + public $left; + + /** + * @var int + * + * @ES\Property(type="integer", name="right") + */ + public $right; + + /** + * @var string + * + * @ES\Property(type="string", name="parent_id", boost=1.0) + */ + public $parentId; + + /** + * @var string + * + * @ES\Property(type="string", name="root_id", boost=1.0, index="not_analyzed") + */ + public $rootId; + + /** + * @var int + * + * @ES\Property(type="integer", name="sort") + */ + public $sort; + + /** + * @var string + * + * @ES\Property(type="string", name="folder", index="not_analyzed") + */ + public $folder; + + /** + * @var string + * + * @ES\Property(type="string", name="content", boost=2.0) + */ + public $content; + + /** + * @var bool + * + * @ES\Property(type="boolean", name="hidden") + */ + public $hidden; + + /** + * @var bool + */ + public $selected; + + /** + * Assigns multiple fields from array, just for test. + * + * @param array $data + */ + public function assign($data) + { + foreach ($data as $key => $value) { + $this->$key = $value; + } + } +} diff --git a/Tests/app/fixture/Acme/TestBundle/Document/Product.php b/Tests/app/fixture/Acme/TestBundle/Document/Product.php new file mode 100644 index 0000000..8ede320 --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/Document/Product.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Tests\app\fixture\Acme\TestBundle\Document; + +use ONGR\ElasticsearchBundle\Annotation as ES; +use ONGR\ElasticsearchBundle\Document\DocumentInterface; +use ONGR\ElasticsearchBundle\Document\DocumentTrait; + +/** + * Dummy product document. + * + * @ES\Document(type="product") + */ +class Product implements DocumentInterface +{ + use DocumentTrait; +} diff --git a/Tests/app/fixture/Acme/TestBundle/Resources/config/services.yml b/Tests/app/fixture/Acme/TestBundle/Resources/config/services.yml new file mode 100644 index 0000000..12d183a --- /dev/null +++ b/Tests/app/fixture/Acme/TestBundle/Resources/config/services.yml @@ -0,0 +1,4 @@ +parameters: + +services: + diff --git a/Twig/CategoryExtension.php b/Twig/CategoryExtension.php new file mode 100755 index 0000000..0b87e5e --- /dev/null +++ b/Twig/CategoryExtension.php @@ -0,0 +1,233 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Twig; + +use ONGR\ContentBundle\Service\CategoryService; + +/** + * CategoryExtension class. + */ +class CategoryExtension extends \Twig_Extension +{ + const NAME = 'category_extension'; + + /** + * @var CategoryService + */ + private $categoryService; + + /** + * Max category level to render to. + * + * @var int + */ + private $maxLevel = null; + + /** + * Category template for render. + * + * @var string + */ + private $template = null; + + /** + * @param CategoryService $service + */ + public function __construct($service) + { + $this->categoryService = $service; + } + + /** + * Get name of the twig extension. + * + * @return string + */ + public function getName() + { + $name = self::NAME; + + return $name; + } + + /** + * Sets template name for the renderer. + * + * @param string $template + */ + public function setTemplate($template) + { + $this->template = $template; + } + + /** + * Gets template for the renderer. + * + * @return null|string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Sets max level. + * + * @param int $maxLevel + */ + public function setMaxLevel($maxLevel) + { + $this->maxLevel = $maxLevel; + } + + /** + * Gets max level. + * + * @return null|int + */ + public function getMaxLevel() + { + return $this->maxLevel; + } + + /** + * Returns a list of functions to add to the existing list. + * + * @return array An array of functions + */ + public function getFunctions() + { + return [ + 'category_tree' => new \Twig_SimpleFunction( + 'getCategoryTree', + [ + $this, + 'getCategoryTree', + ], + [ + 'needs_environment' => true, + 'is_safe' => array('html'), + ] + ), + 'render_tree' => new \Twig_SimpleFunction( + 'renderCategoryTree', + [ + $this, + 'renderCategoryTree', + ], + [ + 'needs_environment' => true, + 'is_safe' => array('html'), + ] + ), + 'category_child_tree' => new \Twig_SimpleFunction( + 'getCategoryChildTree', + [ + $this, + 'getCategoryChildTree', + ], + [ + 'needs_environment' => true, + 'is_safe' => array('html'), + ] + ), + ]; + } + + /** + * Renders category tree. + * + * @param \Twig_Environment $environment + * @param array $tree + * @param string|null $selectedCategory + * @param string|null $currentCategory + * @param string|null $template + * + * @return null|string + */ + public function renderCategoryTree( + \Twig_Environment $environment, + $tree, + $selectedCategory = null, + $currentCategory = null, + $template = null + ) { + if (count($tree)) { + if ($template === null) { + $template = $this->getTemplate(); + } + + return $environment->render( + $template, + [ + 'categories' => $tree, + 'selected_category' => $selectedCategory, + 'current_category' => $currentCategory, + ] + ); + } + + return null; + } + + /** + * Returns rendered category tree. + * + * @param \Twig_Environment $environment + * @param string $template + * @param int $maxLevel + * @param null|string $selectedCategory + * + * @return null|string + */ + public function getCategoryTree( + \Twig_Environment $environment, + $template = 'ONGRContentBundle:Category:inc/categorytree.html.twig', + $maxLevel = 0, + $selectedCategory = null + ) { + $this->setMaxLevel($maxLevel); + $this->setTemplate($template); + + $this->categoryService->setCurrentCategoryId($selectedCategory); + $tree = $this->categoryService->getTree($this->getMaxLevel(), true); + $currentCategory = $this->categoryService->getCurrentCategoryDocument(); + + return $this->renderCategoryTree($environment, $tree, $selectedCategory, $currentCategory); + } + + /** + * Returns rendered category tree. + * + * @param \Twig_Environment $environment + * @param string $template + * @param int $maxLevel + * @param null|string $selectedCategory + * @param null $fromCategory + * + * @return null|string + */ + public function getCategoryChildTree( + \Twig_Environment $environment, + $template = 'ONGRCategoryBundle:Category:inc/categorytree.html.twig', + $maxLevel = 0, + $selectedCategory = null, + $fromCategory = null + ) { + $this->categoryService->setCurrentCategoryId($selectedCategory); + + $tree = $this->categoryService->getPartialTree($maxLevel, $fromCategory); + $currentCategory = $this->categoryService->getCurrentCategoryDocument(); + + return $this->renderCategoryTree($environment, $tree, $selectedCategory, $currentCategory, $template); + } +} diff --git a/Twig/ContentExtension.php b/Twig/ContentExtension.php new file mode 100755 index 0000000..012697e --- /dev/null +++ b/Twig/ContentExtension.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ONGR\ContentBundle\Twig; + +use ONGR\ElasticsearchBundle\DSL\Query\TermsQuery; +use ONGR\ElasticsearchBundle\ORM\Repository; +use Symfony\Component\HttpKernel\Fragment\FragmentHandler; +use Symfony\Component\Routing\RouterInterface; + +/** + * ContentExtension class. + */ +class ContentExtension extends \Twig_Extension +{ + const NAME = 'content_extension'; + + /** + * @var Repository + */ + protected $repository; + + /** + * @var FragmentHandler + */ + protected $handler; + + /** + * @var RouterInterface + */ + protected $router; + + /** + * @var string + */ + protected $renderStrategy; + + /** + * @param FragmentHandler $handler + * @param RouterInterface $router + * @param string $renderStrategy + * @param Repository $repository + */ + public function __construct( + $handler = null, + RouterInterface $router = null, + $renderStrategy = null, + $repository = null + ) { + $this->handler = $handler; + $this->router = $router; + $this->renderStrategy = $renderStrategy; + $this->repository = $repository; + } + + /** + * Provide a list of helper functions to be used. + * + * @return array + */ + public function getFunctions() + { + return [ + 'getContentsBySlugs' => new \Twig_SimpleFunction( + 'getContentsBySlugs', + [ + $this, + 'getContentsBySlugsFunction', + ], + [ + 'needs_environment' => false, + 'is_safe' => ['html'], + ] + ), + 'snippet' => new \Twig_SimpleFunction( + 'snippet', + [ + $this, + 'snippetFunction', + ], + [ + 'is_safe' => ['html'], + ] + ), + ]; + } + + /** + * Return an array with content documents filtered by slugs array. + * + * @param array $slugs + * @param bool $keepOrder + * + * @return mixed + */ + public function getContentsBySlugsFunction($slugs, $keepOrder = false) + { + $search = $this->repository->createSearch(); + $search->addQuery(new TermsQuery('slug', $slugs), 'should'); + + $result = $this->repository->execute($search); + + if ($keepOrder) { + $orderedResult = array(); + foreach ($slugs as $slug) { + foreach ($result as $document) { + if ($document->slug == $slug) { + $orderedResult[] = $document; + } + } + } + + $result = $orderedResult; + } + + return $result; + } + + /** + * Renders content by given slug. + * + * @param string $slug + * @param bool|string $template + * + * @return string + */ + public function snippetFunction( + $slug, + $template = null + ) { + $result = null; + if ($this->handler && $this->router) { + $route = $this->router->generate( + '_ongr_plain_cms_snippet', + [ + 'slug' => $slug, + 'template' => $template, + ] + ); + try { + $result = $this->handler->render($route, $this->renderStrategy); + } catch (\InvalidArgumentException $ex) { + // ESI is disabled. + $result = $this->handler->render($route); + } + } + + return $result; + } + + /** + * Get name of the twig extension. + * + * @return string + */ + public function getName() + { + return self::NAME; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..689e9d2 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "ongr/content-bundle", + "description": "Main content bundle to handle category, product and content data.", + "type": "symfony-bundle", + "homepage": "http://ongr.io", + "license": "MIT", + "authors": [ + { + "name": "ONGR team", + "homepage": "http://www.nfq.com" + } + ], + "require": { + "php": ">=5.4", + "symfony/symfony": "~2.3", + "ongr/elasticsearch-bundle": "~0.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.1", + "squizlabs/php_codesniffer": "~1.5", + "ongr/router-bundle": "~0.1" + }, + "suggest": { + "ongr/router-bundle": "Adds SEO friendly URLs" + }, + "autoload": { + "psr-4": { "ONGR\\ContentBundle\\": "" } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..2aa5b4d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,45 @@ + + + + + + ./Tests/Functional/ + + + ./Tests/Unit/ + + + ./Tests/ + + + + + + + + + + ./ + + ./Tests + ./vendor + + + + + + + + + +