Skip to content

Instantly share code, notes, and snippets.

@wizhippo
Last active August 29, 2015 14:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wizhippo/823d8b5b8455b26ad310 to your computer and use it in GitHub Desktop.
Save wizhippo/823d8b5b8455b26ad310 to your computer and use it in GitHub Desktop.
Example menu based on subtree
<?php
namespace Example\Bundle\ExampleBundle\Controller;
use eZ\Bundle\EzPublishCoreBundle\Controller;
use eZ\Publish\API\Repository\Values\Content\Location;
use eZ\Publish\Core\Pagination\Pagerfanta\ContentSearchAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
class ConsumerSiteController extends Controller
{
/**
* Renders the top menu, with cache control
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function topMenuAction()
{
$location = $this->getRootLocation();
// Setting HTTP cache for the response to be public and with a TTL of 1 day.
$response = new Response;
$response->setPublic();
$response->setSharedMaxAge( 86400 );
// Menu will expire when top location cache expires.
$response->headers->set( 'X-Location-Id', $location->id );
// Menu might vary depending on user permissions, so make the cache vary on the user hash.
$response->setVary( 'X-User-Hash' );
// Generate criterion from $excludeContentTypes and pass it to the menu helper.
$excludeCriterion = $this->get( 'ezdemo.criteria_helper' )
->generateContentTypeExcludeCriterion(
// Get contentType identifiers we want to exclude from configuration (see default_settings.yml).
$this->container->getParameter( 'ezdemo.top_menu.content_types_exclude' )
);
$locationTree = $this->get( 'example.menu_helper' )->getTopMenuContent( $location->pathString, $excludeCriterion );
return $this->render(
'ExampleExampleBundle::page_topmenu.html.twig',
array(
'locationTree' => $locationTree,
'collapseTopLevel' => true,
),
$response
);
}
/**
* Renders page header links with cache control
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function userLinksAction()
{
$response = new Response();
$response->setSharedMaxAge( 3600 );
$response->setVary( 'Cookie' );
return $this->render(
"eZDemoBundle::page_header_links.html.twig",
array(),
$response
);
}
/**
* Renders article with extra parameters that controls page elements visibility such as image and summary
*
* @param $locationId
* @param $viewType
* @param bool $layout
* @param array $params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showArticleAction( $locationId, $viewType, $layout = false, array $params = array() )
{
return $this->get( 'ez_content' )->viewLocation(
$locationId,
$viewType,
$layout,
array(
'showSummary' => $this->container->getParameter( 'ezdemo.article.full_view.show_summary' ),
'showImage' => $this->container->getParameter( 'ezdemo.article.full_view.show_image' )
) + $params
);
}
/**
* Displays the list of blog_post
* Note: This is a fully customized controller action, it will generate the response and call
* the view. Since it is not calling the ViewControler we don't need to match a specific
* method signature.
*
* @param int $locationId of a blog
* @return \Symfony\Component\HttpFoundation\Response
*/
public function listBlogPostsAction( $locationId )
{
$response = new Response();
// Setting default cache configuration (you can override it in you siteaccess config)
$response->setSharedMaxAge( $this->getConfigResolver()->getParameter( 'content.default_ttl' ) );
// Make the response location cache aware for the reverse proxy
$response->headers->set( 'X-Location-Id', $locationId );
$response->setVary( 'X-User-Hash' );
$viewParameters = $this->getRequest()->attributes->get( 'viewParameters' );
// TODO keyword search is not implemented in the public API yet, so we forward to a legacy view
if ( !empty( $viewParameters['tag'] ) )
{
$tag = $viewParameters['tag'];
return $this->redirect(
$this->generateUrl(
'ez_legacy',
array( 'module_uri' => '/content/keyword/' . $tag )
)
);
}
// Getting location and content from ezpublish dedicated services
$repository = $this->getRepository();
$location = $repository->getLocationService()->loadLocation( $locationId );
if ( $location->invisible )
{
throw new NotFoundHttpException( "Location #$locationId cannot be displayed as it is flagged as invisible." );
}
$content = $repository
->getContentService()
->loadContentByContentInfo( $location->getContentInfo() );
// Getting language for the current siteaccess
$languages = $this->getConfigResolver()->getParameter( 'languages' );
// Using the criteria helper (a demobundle custom service) to generate our query's criteria.
// This is a good practice in order to have less code in your controller.
$criteria = $this->get( 'ezdemo.criteria_helper' )->generateListBlogPostCriterion(
$location, $viewParameters, $languages
);
// Generating query
$query = new Query();
$query->criterion = $criteria;
$query->sortClauses = array(
new SortClause\Field( 'blog_post', 'publication_date', Query::SORT_DESC, $languages[0] )
);
// Initialize pagination.
$pager = new Pagerfanta(
new ContentSearchAdapter( $query, $this->getRepository()->getSearchService() )
);
$pager->setMaxPerPage( $this->container->getParameter( 'ezdemo.blog.blog_post_list.limit' ) );
$pager->setCurrentPage( $this->getRequest()->get( 'page', 1 ) );
return $this->render(
'eZDemoBundle:full:blog.html.twig',
array(
'location' => $location,
'content' => $content,
'pagerBlog' => $pager
),
$response
);
}
/**
* Action used to display a blog_post
* - Adds the content's author to the response.
* Note: This is a partly customized controller action. It is executed just before the original
* Viewcontroller's viewLocation method. To be able to do that, we need to implement it's
* full signature.
*
* @param $locationId
* @param $viewType
* @param bool $layout
* @param array $params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showBlogPostAction( $locationId, $viewType, $layout = false, array $params = array() )
{
// We need the author, whatever the view type is.
$repository = $this->getRepository();
$location = $repository->getLocationService()->loadLocation( $locationId );
$author = $repository->getUserService()->loadUser( $location->getContentInfo()->ownerId );
// TODO once the keyword service is available, load the number of keyword for each keyword
// Delegate view rendering to the original ViewController
// (makes it possible to continue using defined template rules)
// We just add "author" to the list of variables exposed to the final template
return $this->get( 'ez_content' )->viewLocation(
$locationId,
$viewType,
$layout,
array( 'author' => $author )
);
}
/**
* Displays content having similar tags as the given location
*
* @param \eZ\Publish\API\Repository\Values\Content\Location $location
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewTagRelatedContentAction( Location $location )
{
// TODO once the keyword service is available replace this subrequest by a full symfony one
return $this->render(
'eZDemoBundle:parts:related_content.html.twig',
array( 'location' => $location )
);
}
/**
* Displays description, tagcloud, tags, ezarchive and calendar
* of the parent's of a given location
*
* @param \eZ\Publish\API\Repository\Values\Content\Location $location
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewParentExtraInfoAction( Location $location )
{
$repository = $this->getRepository();
$parentLocation = $repository->getLocationService()->loadLocation( $location->parentLocationId );
// TODO once the keyword service is available replace part this subrequest by a full symfony one
return $this->render(
'eZDemoBundle:parts/blog:extra_info.html.twig',
array( 'location' => $parentLocation )
);
}
/**
* Displays description, tagcloud, tags, ezarchive and calendar for a given location
*
* @param \eZ\Publish\API\Repository\Values\Content\Location $location
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewExtraInfoAction( Location $location )
{
// TODO once the keyword service is available replace part this subrequest by a full symfony one
return $this->render(
'eZDemoBundle:parts/blog:extra_info.html.twig',
array( 'location' => $location )
);
}
/**
* Displays "tip a friend" link for a given location
*
* @param \eZ\Publish\API\Repository\Values\Content\Location $location
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewTipAFriendAction( Location $location )
{
return $this->render(
'eZDemoBundle:parts/article:tip_a_friend.html.twig',
array( 'location' => $location )
);
}
/**
* Displays star rating attribute for a given location
*
* @param \eZ\Publish\API\Repository\Values\Content\Location $location
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewStarRatingAction( Location $location )
{
return $this->render(
'eZDemoBundle:parts/article:star_rating.html.twig',
array( 'location' => $location )
);
}
}
<?php
namespace Example\Bundle\ExampleBundle\Helper;
use eZ\Publish\API\Repository\Repository;
use eZ\Publish\API\Repository\Values\Content\Location;
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
/**
* Helper for menus
*/
class MenuHelper
{
/**
* @var \eZ\Publish\API\Repository\Repository
*/
private $repository;
/**
* Default limit for content list in menus.
*
* @var int
*/
private $defaultMenuLimit;
/**
* Default limit for depth list in menus.
*
* @var int
*/
private $defaultMenuDepth;
/**
* @var \Tocom\Bundle\ConsumerSiteBundle\Helper\SearchHelper
*/
private $searchHelper;
public function __construct( Repository $repository, $defaultMenuLimit, $defaultMenuDepth, SearchHelper $searchHelper )
{
$this->repository = $repository;
$this->defaultMenuLimit = $defaultMenuLimit;
$this->defaultMenuDepth = $defaultMenuDepth;
$this->searchHelper = $searchHelper;
}
/**
* Returns tree of location objects that we want to display in top menu, based on $topLocationId.
* All location objects are fetched under $subtree only.
*
* One might use $excludeContentTypeIdentifiers to explicitly exclude some content types (e.g. "article").
*
* @param $subtree
* @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion Additional criterion for filtering.
* @param int $depth Maximum tree depth
*
* @return array of tree array("location" => location , "children" => array(
* array("location" => location , "children" => ...),
* array("location" => location , "children" => ...),
* )
*/
public function getTopMenuContent( $subtree, Criterion $criterion = null, $depth = null )
{
$depth = $depth ?: $this->defaultMenuDepth;
$criteria = array(
new Criterion\Subtree( $subtree ),
new Criterion\Location\Depth( Operator::LTE, $depth ),
new Criterion\Visibility( Criterion\Visibility::VISIBLE )
);
if ( !empty( $criterion ) )
$criteria[] = $criterion;
$query = new LocationQuery(
array(
'criterion' => new Criterion\LogicalAnd( $criteria ),
'sortClauses' => array( new SortClause\Location\Priority( Query::SORT_ASC ) )
)
);
$query->limit = $this->defaultMenuLimit;
return $this->searchHelper->buildLocationTreeFromSearchResult( $subtree, $this->repository->getSearchService()->findLocations( $query ) );
}
/**
* Returns latest published content that is located under $pathString and matching $contentTypeIdentifier.
* The whole subtree will be passed through to find content.
*
* @param \eZ\Publish\API\Repository\Values\Content\Location $rootLocation Root location we want to start content search from.
* @param string[] $includeContentTypeIdentifiers Array of ContentType identifiers we want content to match.
* @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion Additional criterion for filtering.
* @param int|null $limit Max number of items to retrieve. If not provided, default limit will be used.
*
* @return \eZ\Publish\API\Repository\Values\Content\Content[]
*/
public function getLatestContent( Location $rootLocation, array $includeContentTypeIdentifiers = array(), Criterion $criterion = null, $limit = null )
{
$criteria = array(
new Criterion\Subtree( $rootLocation->pathString ),
new Criterion\Visibility( Criterion\Visibility::VISIBLE )
);
if ( $includeContentTypeIdentifiers )
$criteria[] = new Criterion\ContentTypeIdentifier( $includeContentTypeIdentifiers );
if ( !empty( $criterion ) )
$criteria[] = $criterion;
$query = new Query(
array(
'criterion' => new Criterion\LogicalAnd( $criteria ),
'sortClauses' => array( new SortClause\DatePublished( Query::SORT_DESC ) )
)
);
$query->limit = $limit ?: $this->defaultMenuLimit;
return $this->searchHelper->buildListFromSearchResult( $this->repository->getSearchService()->findContent( $query ) );
}
}
<ul class="nav navbar-nav">
{% if collapseTopLevel %}
<li id="nav-location-{{ locationTree.location.id }}">
<a href="{{ path( locationTree.location ) }}">{{ ez_content_name( locationTree.location.contentInfo ) }}</a>
</li>
{% if locationTree.children is not null %}
{% for child in locationTree.children %}
{% include 'ExampleExampleBundle::page_topmenu_links.html.twig' with {'item': child} %}
{% endfor %}
{% endif %}
{% else %}
{% include 'ExampleExampleBundle::page_topmenu_links.html.twig' with {'item': locationTree} %}
{% endif %}
</ul>
<?php
namespace Example\Bundle\ExampleBundle\Helper;
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
/**
* Helper for searches
*/
class SearchHelper
{
/**
* Builds a Content list from $searchResult.
* Returned array consists of a hash of Content objects, indexed by their ID.
*
* @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $searchResult
*
* @return array
*/
public function buildListFromSearchResult( SearchResult $searchResult )
{
$list = array();
foreach ( $searchResult->searchHits as $searchHit )
{
$list[$searchHit->valueObject->contentInfo->id] = $searchHit->valueObject;
}
return $list;
}
/**
* Builds a tree from $searchResult.
*
* @param $subtree to start from
* @param \eZ\Publish\API\Repository\Values\Content\Search\SearchResult $searchResult
*
* @return array of tree array("location" => location , "children" => array(
* array("location" => location , "children" => ...),
* array("location" => location , "children" => ...),
* )
*/
public function buildLocationTreeFromSearchResult( $subtree, SearchResult $searchResult )
{
$baseCount = strlen($subtree) - strlen(str_replace(str_split("/"), '', $subtree)) - 1;
$tree = array('location' => null, 'children' => null);
foreach ( $searchResult->searchHits as $searchHit )
{
$path = array_slice($searchHit->valueObject->path, $baseCount);
$leaf = &$tree;
while ($pathElement = array_shift($path)) {
if (!isset($leaf['children'][$pathElement])) {
$leaf['children'][$pathElement] = array('location' => null, 'children' => null);
}
$leaf = &$leaf['children'][$pathElement];
}
$leaf['location'] = $searchHit->valueObject;
}
return $tree;
}
}
parameters:
example.menu_helper.class: Example\Bundle\ExampleBundle\Helper\MenuHelper
example.search_helper.class: Example\Bundle\ExampleBundle\Helper\SearchHelper
example.menu_helper.default_limit: 10
example.menu_helper.default_depth: 3
services:
example.menu_helper:
class: %example.menu_helper.class%
arguments: [@ezpublish.api.repository, %example.menu_helper.default_limit%, %example.menu_helper.default_depth%, @example.search_helper]
example.search_helper:
class: %example.search_helper.class%
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment