Partial Content

Working with eZ Publish 5 - Listeners

Our blog is starting to look a lot like a blog, but we'd like to do better than "This Site's Name" and "sidebar goes here" for the surround.

You could hardcode the site name and sidebar into the templates, but I'd prefer to have those things editable from the admin interface with the rest of the site. It's common practice on an eZ Publish site to have a 'Design' or 'Site Surround' object that holds some of your common settings, and that's what we'll do here. So how do we get this object in to our template?

One approach would be to use sub-requests, and in fact that's what eZ's demo bundle does. But if we're pulling a lot of different pieces, that's a lot of controller methods, sub-requests, and templates. I'd prefer to just have the surround object available all the time in my template. Based on what we've looked at so far, though, the only way to do that would be to write controller methods for all my pages. No thanks.

Fortunately, there's another new piece of functionality available that we haven't examined yet. eZ Publish 5 provides a number of events that can be hooked to during the page generation process to provide your own code. In particular, there's a PreContentView event that eZ Publish triggers just before a page renders that gives you the opportunity to insert more variables into a page. That sounds like a good approach, so let's hook that event and add our surround object to the template.

First, let's do the usual work-resetting dance of updating git to the next tutorial step:

git checkout -- *
git clean -f -d
git checkout -b tutorial-5 origin/tutorial-5

We'll need to write a listener that accesses the content repository using the Public API (just like our controller), gets the surround object, and adds it to the variables in the template. But how will it know which object to retrieve? We could hardcode that into the listener, but instead let's also make a new config setting to tell our listener which object type is the surround object.

Let's add the following to ezpublish/config/parameters.yml, under and in line withe the existing parameters there:

     ez5tutorial.default.surround_type: template_look

'template_look' is the name for this surround/design object in most eZ Publish repositories.

Add a new directory to your project, src/Blend/TutorialBlogBundle/EventListener, and create a new php file in it, PreContentViewListener.php. Add the following code:

?php
namespace Blend\TutorialBlogBundle\EventListener;

use eZ\Publish\Core\MVC\ConfigResolverInterface,
    eZ\Publish\Core\MVC\Symfony\Event\PreContentViewEvent,
    eZ\Publish\API\Repository\Values\Content\Query,
    eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator,
    eZ\Publish\API\Repository\Values\Content\Query\Criterion,
    eZ\Publish\API\Repository\Values\Content\Query\SortClause,
    eZ\Publish\API\Repository\Repository;

/**
 * PreContentViewListener hooks the PreContentView Event to provide extra data to the template
 */
class PreContentViewListener
{
    /**
     * @var \eZ\Publish\Core\MVC\ConfigResolverInterface
     */
    protected $configResolver;

    /**
     * @var \eZ\Publish\API\Repository\Repository
     */
    protected $repository;


    /**
     * Constructs our listener and loads it with access to the eZ Publish repository and config
     * @param \eZ\Publish\API\Repository\Repository $repository
     * @param \eZ\Publish\Core\MVC\ConfigResolverInterface $configResolver
     */
    public function __construct( Repository $repository, ConfigResolverInterface $configResolver )
    {
        //Add these to the class so we have them when the event method is triggered
        $this->repository = $repository;
        $this->configResolver = $configResolver;
    }

    /**
     * Fires just before the page is rendered
     * @param \eZ\Publish\Core\MVC\Symfony\Event\PreContentViewEvent $event
     */
    public function onPreContentView( PreContentViewEvent $event )
    {
        //What's our design/surround object in the repository called? Check the config
        //This reads the setting we added to parameters.yml
        $surroundTypeIdentifier = $this->configResolver->getParameter('surround_type', 'ez5tutorial');

        //To retrieve the surround object, first access the repository
        $searchService = $this->repository->getSearchService();

        //Find the first object that matched the name from our config
        //(We only expect there to be one in the DB)
        $surround = $searchService->findSingle(
            new Criterion\ContentTypeIdentifier($surroundTypeIdentifier)
        );

        //Retrieve the view context from the event
        $contentView = $event->getContentView();

        //Add the surround variable to the context
        $contentView->addParameters( array('surround' => $surround) );
    }
}

Some of this should look familiar from the Controller we made earlier. We access the repository's Search service, give it our criteria, and find an object. Some of these pieces, though, are new. The ConfigResolver is your way of accessing the configuration variables defined in the various yml files. The service helpfully works out for you whether you're in dev or prod, what siteaccess you may be running (etc), and returns the correct value. In this case, we're retrieving the ini setting we added above.

The constructor poses a puzzle, though: how do we construct this listener and make sure it's loading the services it needs? Here the dependency injection container comes to the rescue again. Let's update src/Blend/TutorialBlogBundle/Resources/config/services.yml with some new instructions. Add this line to the parameters section: 

    blend_tutorial_blog.pre_content_view_listener.class: Blend\TutorialBlogBundle\EventListener\PreContentViewListener

and add this to the service section:

    #define our event listener
    blend_tutorial_blog.pre_content_view_listener:
        class: %blend_tutorial_blog.pre_content_view_listener.class%
        #These services will be passed to the constructor
        arguments: [@ezpublish.api.repository, @ezpublish.config.resolver]
        #This tag tells the framework we're interested in the PreContentView event
        tags:
            - {name: kernel.event_listener, event: ezpublish.pre_content_view, method: onPreContentView}

With these pieces added, Symfony will now automatically instantiate our class and give it the right components. In the arguments section, the '@ezpublish.api.repository' and '@ezpublish.config.resolver' are service markers that tell Symfony to insert the objects for those services. So as far as our code is concerned, those services could be easily replaced with something else, as long as the interfaces stayed the same.

Let's give it a try: add this somewhere within the body on your pagelayout.html.twig:

{{ dump(surround) }} 

You should get a long variable dump listing, including a list of fields. This is your surround object, inserted by the listener. Go ahead and remove the dump command, then let's modify the template to take advantage of it (here's the whole pagelayout.html.twig again, with tags added for {{surround}}.

{# Base HTML based on HTML Boilerplate and Twitter Bootstrap #}
<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>{{ location.contentInfo.name }} - {{ surround.contentInfo.name }}</title>
    <meta name="description" content="{{ surround.fields.meta_description['eng-US'].text }}">
    <meta name="viewport" content="width=device-width">

    {% stylesheets "bundles/blendtutorialblog/css/*" %}
        <link rel="stylesheet" href="{{ asset_url }}" />
    {% endstylesheets %}

    {% javascripts
        "bundles/blendtutorialblog/js/modernizr.js"
    %}
        <script type="text/javascript" src="{{ asset_url }}"></script>
    {% endjavascripts %}
</head>
<body>
<header class="main-header">
    {# The block defines a piece of the template that can be overridden by other templates #}
    {# See http://twig.sensiolabs.org/doc/templates.html#template-inheritance #}
    {% block header %}
        <div class="branding jumbotron subhead">
            <div class="container">
                <h1>{{ ez_render_field(surround, "title") }}</h1>
            </div>
        </div>
        <nav class="navbar navbar-inverse">
            <div class="navbar-inner">
                <div class="container">
                    {# You can nest blocks #}
                    {% block topnav %}
                        Top Menu will go here
                    {% endblock %}
                </div>
            </div>
        </nav>
    {% endblock %}
</header>

<div class="container">
    <div class="body">
        <div class="row">
            <div class="main span9">
                {# eZ Publish uses the 'content' block as the main content of the page #}
                {% block content %}{% endblock %}
            </div>
            <aside class="sidebar span3">
                {% block sidebar %}
                    {{ ez_render_field(surround, "sidebar") }}
                {% endblock %}
            </aside>
        </div>
    </div>
</div>

<footer class="footer">
    {% block footer %}
        {{ ez_render_field(surround, "footer") }}
    {% endblock %}
</footer>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
{% javascripts
    "bundles/blendtutorialblog/js/bootstrap.js"
    "bundles/blendtutorialblog/js/main.js"
%}
    <script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}

</body>
</html>

Let's shift-reload the site and see how things came together: 

Screenshot with surround object

Not bad! You can edit the surround settings under the 'Design' top link in the admin. Note that as you make edits, the frontend may not change. This is because the surround object isn't taken into account by the modification dates and Etags sent out by our controllers. You could modify the code to factor it in, but it isn't likely to change often, so it's probably easier to just manually clear the cache (ezpublish/console cache:clear -e dev)