Sie sind auf Seite 1von 169

The Symfony CMF Book

for Symfony master


generated on July 18, 2013

The Symfony CMF Book (master) This work is licensed under the Attribution-Share Alike 3.0 Unported license (http://creativecommons.org/ licenses/by-sa/3.0/). You are free to share (to copy, distribute and transmit the work), and to remix (to adapt the work) under the following conditions: Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). Share Alike: If you alter, transform, or build upon this work, you may distribute the resulting work only under the same, similar or a compatible license. For any reuse or distribution, you must make clear to others the license terms of this work. The information in this book is distributed on an as is basis, without warranty. Although every precaution has been taken in the preparation of this work, neither the author(s) nor SensioLabs shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in this work. If you find typos or errors, feel free to report them by creating a ticket on the Symfony ticketing system (http://github.com/symfony/symfony-docs/issues). Based on tickets and users feedback, this book is continuously updated.

Contents at a Glance
Installing the Symfony CMF Standard Edition .....................................................................................5 Routing ..............................................................................................................................................9 Content ............................................................................................................................................15 Menu................................................................................................................................................17 SimpleCMS.......................................................................................................................................20 Choosing a Storage Layer ..................................................................................................................25 Installing and Configuring the CMF Core ..........................................................................................29 Installing and Configuring Doctrine PHPCR-ODM ............................................................................32 Installing and Configuring Inline Editing ...........................................................................................39 Creating a CMS using CMF and Sonata .............................................................................................41 Using the BlockBundle and ContentBundle with PHPCR ...................................................................45 Handling Multi-Language Documents ...............................................................................................56 The BlockBundle ..............................................................................................................................60 Block Types ......................................................................................................................................66 Create your own Blocks ....................................................................................................................71 Cache ...............................................................................................................................................73 Relation to Sonata Block Bundle........................................................................................................78 BlogBundle .......................................................................................................................................79 ContentBundle .................................................................................................................................83 CoreBundle ......................................................................................................................................84 CreateBundle ....................................................................................................................................92 DoctrinePHPCRBundle .....................................................................................................................99 MenuBundle ................................................................................................................................... 112 RoutingBundle................................................................................................................................ 118 RoutingAutoBundle ........................................................................................................................ 125 SearchBundle .................................................................................................................................. 133 SimpleCmsBundle........................................................................................................................... 134 SonataDoctrinePhpcrAdminBundle ................................................................................................. 138 TreeBrowserBundle......................................................................................................................... 140 Using a Custom Document Class Mapper with PHPCR-ODM ......................................................... 144 Using a Custom Route Repository with Dynamic Router.................................................................. 145 Installing the CMF sandbox ............................................................................................................ 147 Routing .......................................................................................................................................... 152 Testing ........................................................................................................................................... 157 Contributing................................................................................................................................... 163 The Symfony CMF Release Process.................................................................................................. 164
PDF brought to you by generated on July 18, 2013

Contents at a Glance | iii

Licensing ........................................................................................................................................ 166

iv | Contents at a Glance

Contents at a Glance | 4

Chapter 1

Installing the Symfony CMF Standard Edition


The Symfony CMF Standard Edition (SE) is a distribution based on all the main components needed for most common use cases. The goal of this tutorial is to install the CMF components, with the minimum necessary configuration and some very simple examples, into a working Symfony2 application. We will then walk you through the components you have installed. This can be used to familiarize yourself with the CMF or as a starting point for a new custom application. If this is your first encounter with the Symfony CMF it would be a good idea to first take a look at: The Big Picture1 The online sandbox demo at cmf.liip.ch2
For other Symfony CMF installation guides, please read: The cookbook entry on Installing the CMF sandbox for instructions on how to install a more complete demo instance of Symfony CMF. Installing and Configuring the CMF Core for step-by-step installation and configuration details of just the core components into an existing Symfony application.

Preconditions
As Symfony CMF is based on Symfony2, you should make sure you meet the Requirements for running Symfony23. Additionally, you need to have SQLite4 PDO extension (pdo_sqlite) installed, since it is used as the default storage medium.

1. http://slides.liip.ch/static/2012-01-17_symfony_cmf_big_picture.html#1 2. http://cmf.liip.ch 3. http://symfony.com/doc/current/reference/requirements.html 4. http://www.sqlite.org/

PDF brought to you by generated on July 18, 2013

Chapter 1: Installing the Symfony CMF Standard Edition | 5

By default, Symfony CMF uses Jackalope + Doctrine DBAL and SQLite as the underlying DB. However, Symfony CMF is storage agnostic, which means you can use one of several available data storage mechanisms without having to rewrite your code. For more information on the different available mechanisms and how to install and configure them, refer to Installing and Configuring Doctrine PHPCR-ODM

Git5 and Curl6 are also needed to follow the installation steps listed below.

Installation
The easiest way to install Symfony CMF is is using Composer7. Get it using
Listing 1-1

1 $ curl -sS https://getcomposer.org/installer | php 2 $ sudo mv composer.phar /usr/local/bin/composer

and then get the Symfony CMF code with it (this may take a while)
Listing 1-2

1 $ php composer.phar create-project symfony-cmf/standard-edition <path-to-install> 2 --stability=dev $ cd <path-to-install>

The path <path-to-install> should either inside your web server doc root or configure a virtual host for <path-to-install>.

This will clone the standard edition and install all the dependencies and run some initial commands. These commands require write permissions to the app/cache and app/logs directory. In case the final commands end up giving permissions errors, please follow the guidelines in the symfony book8 for configuring the permissions and then run the composer.phar install command mentioned below. If you prefer you can also just clone the project:
Listing 1-3

1 $ git clone git://github.com/symfony-cmf/symfony-cmf-standard.git <dir-name> 2 $ cd <dir-name>

If there were problems during the create-project command, if you used git clone or if you updated the checkout later, always run the following command to update the dependencies:
Listing 1-4

1 $ php composer.phar install

The next step is to set up the database. If you want to use SQLite as your database backend just go ahead and run the following:
Listing 1-5

1 $ php app/console doctrine:database:create 2 $ php app/console doctrine:phpcr:init:dbal

5. http://git-scm.com/ 6. http://curl.haxx.se/ 7. http://getcomposer.org/ 8. http://symfony.com/doc/master/book/installation.html#configuration-and-setup

PDF brought to you by generated on July 18, 2013

Chapter 1: Installing the Symfony CMF Standard Edition | 6

3 $ php app/console doctrine:phpcr:repository:init 4 $ php app/console doctrine:phpcr:fixtures:load

This will create a file called app.sqlite inside your app folder, containing the database content. The project should now be accessible on your web server. If you have PHP 5.4 installed you can alternatively use the PHP internal web server:
Listing 1-6

1 $ php app/console server:run

And then access the CMF via:


Listing 1-7

1 http://localhost:8000

If you prefer to use another database backend, for example MySQL, run the configurator (point your browser to /web/config.php) or set your database connection parameters in app/config/ parameters.yml. Make sure you leave the database_path property at null in order to use another driver than SQLite. Leaving the field blank in the web-configurator should set it to null.

Overview
This section will help you understand the basic parts of Symfony CMF Standard Edition (SE) and how they work together to provide the default pages you can see when browsing the Symfony CMF SE installation. It assumes you have already installed Symfony CMF SE and have carefully read the Symfony2 book9.

AcmeMainBundle and SimpleCMSBundle


Symfony CMF SE comes with a default AcmeMainBundle to help you get started, similar to the AcmeDemoBundle provided by Symfony2. This gives you some demo pages viewable in your browser.

Where are the Controllers?


AcmeMainBundle doesn't include controllers or configuration files as you might expect. It contains little more than a Twig file and Fixtures10 data that was loaded into your database during installation. The controller logic is actually provided by the relevant CMF bundles, as described below.

There are several bundles working together in order to turn the fixture data into a browsable website. The overall, simplified process is: When a request is received, the Symfony CMF Routing's Dynamic Router is used to handle the incoming request; The Dynamic Router is able to match the requested URL with a specific ContentBundle's Content stored in the database; The retrieved content's information is used to determine which controller to pass it on to, and which template to use; As configured, the retrieved content is passed to ContentBundle's ContentController, which will handle it and render AcmeMainBundle's layout.html.twig.
9. http://symfony.com/doc/current/book/ 10. http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

PDF brought to you by generated on July 18, 2013

Chapter 1: Installing the Symfony CMF Standard Edition | 7

Again, this is simplified view of a very simple CMS built on top of Symfony CMF. To fully understand all the possibilities of the CMF, a careful look into each component is needed. If you want to review the contents of the PHPCR database you can use the following commands:
Listing 1-8

1 $ php app/console doctrine:phpcr:dump 2 $ php app/console doctrine:phpcr:dump --props 3 $ php app/console doctrine:phpcr:dump /path/to/node

The above examples respectively show a summary, a detailed view, and a summary of a node and all its children (instead of starting at the root node). Don't forget to look at the --help output for more possibilities:
Listing 1-9

1 $ php app/console doctrine:phpcr:dump

Adding new pages


Symfony CMF SE does not provide any admin tools to create new pages. If you are interested in adding an admin UI have a look at Creating a CMS using CMF and Sonata. However if all you want is a simple way to add new pages that you can then edit via the inline editing, then you can use the SimpleCmsBundle page migrator. The Symfony CMF SE ships with an example YAML file stored in app/Resources/data/ pages/test.yml. The contents of this file can be loaded into the PHPCR database by calling:
Listing 1-10

1 $ php app/console doctrine:phpcr:migrator page --identifier=/cms/simple/test

Note that the above identifier is mapped to app/Resources/data/pages/test.yml by stripping off the basepath configuration of the SimpleCmsBundle, which defaults to /cms/simple. Therefore if you want to define a child page foo for /cms/simple/test you would need to create a file app/Resources/data/ pages/test/foo.yml and then run the following command:
Listing 1-11

1 $ php app/console doctrine:phpcr:migrator page --identifier=/cms/simple/test/foo

PDF brought to you by generated on July 18, 2013

Chapter 1: Installing the Symfony CMF Standard Edition | 8

Chapter 2

Routing
This is an introduction to understand the concepts behind CMF routing. For the reference documentation please see Routing and RoutingBundle.

Concept
Why a new Routing Mechanism?
CMS are highly dynamic sites, where most of the content is managed by the administrators rather than developers. The number of available pages can easily reach the thousands, which is usually multiplied by the number of available translations. Best accessibility and SEO practices, as well as user preferences dictate that the URLs should be definable by the content managers. The default Symfony2 routing mechanism, with its configuration file approach, is not the best solution for this problem. It does not provide a way of handling dynamic, user-defined routes, nor does it scale well to a large number of routes.

The Solution
In order to address these issues, a new routing system was developed that takes into account the typical needs of CMS routing: User-defined URLs; Multi-site; Multi-language; Tree-like structure for easier management; Content, Menu and Route separation for added flexibility.

With these requirements in mind, the Symfony CMF Routing component was developed.

PDF brought to you by generated on July 18, 2013

Chapter 2: Routing | 9

The ChainRouter
At the core of Symfony CMF's Routing component sits the ChainRouter. It is used as a replacement for Symfony2's default routing system and, like the Symfony2 router, is responsible for determining which Controller will handle each request. The ChainRouter works by accepting a set of prioritized routing strategies, RouterInterface1 implementations, commonly referred to as "Routers". The routers are responsible for matching an incoming request to an actual Controller and, to do so, the ChainRouter iterates over the configured Routers according to their configured priority:
Listing 2-1

1 # app/config/config.yml 2 cmf_routing: 3 chain: 4 routers_by_id: 5 # enable the DynamicRouter with high priority to allow overwriting 6 # configured routes with content 7 cmf_routing.dynamic_router: 200 8 9 # enable the symfony default router with a lower priority 10 router.default: 100

You can also load Routers using tagged services, by using the router tag and an optional priority. The higher the priority, the earlier your router will be asked to match the route. If you do not specify the priority, your router will come last. If there are several routers with the same priority, the order between them is undetermined. The tagged service will look like this:
Listing 2-2

1 services: 2 my_namespace.my_router: 3 class: "%my_namespace.my_router_class%" 4 tags: 5 - { name: router, priority: 300 }

The Symfony CMF Routing system adds a new DynamicRouter, which complements the default Router found in Symfony2.

The Default Symfony2 Router


Although it replaces the default routing mechanism, Symfony CMF Routing allows you to keep using the existing system. In fact, the default routing is enabled by default, so you can keep using the routes you declared in your configuration files, or as declared by other bundles.

The DynamicRouter
This Router can dynamically load Route instances from a given provider. It then uses a matching process to the incoming request to a specific Route, which in turn is used to determine which Controller to forward the request to. The bundle's default configuration states that DynamicRouter is disabled by default. To activate it, just add the following to your configuration file:
Listing 2-3

1. http://api.symfony.com/master/Symfony/Component/Routing/RouterInterface.html

PDF brought to you by generated on July 18, 2013

Chapter 2: Routing | 10

1 # app/config/config.yml 2 cmf_routing: 3 dynamic: 4 enabled: true

This is the minimum configuration required to load the DynamicRouter as a service, thus making it capable of performing any routing. Actually, when you browse the default pages that come with the Symfony CMF SE, it is the DynamicRouter that matches your requests with the Controllers and Templates.

Getting the Route Object


The provider to use can be configured to best suit each implementation's needs, and must implement the RouteProviderInterface. As part of this bundle, an implementation for PHPCR-ODM2 is provided. Also, you can easily create your own, as the Router itself is storage agnostic. The default provider loads the route at the path in the request and all parent paths to allow for some of the path segments being parameters. For more detailed information on this implementation and how you can customize or extend it, refer to RoutingBundle. The DynamicRouter is able to match the incoming request to a Route object from the underlying provider. The details on how this matching process is carried out can be found in the Routing.
To have the route provider find routes, you also need to provide the data in your storage. With PHPCR-ODM, this is either done through the admin interface (see at the bottom) or with fixtures. However, before we can explain how to do that, you need to understand how the DynamicRouter works. An example will come later in this document.

Getting the Controller and Template


A Route needs to specify which Controller should handle a specific Request. The DynamicRouter uses one of several possible methods to determine it (in order of precedence): Explicit: The stored Route document itself can explicitly declare the target Controller by specifying the '_controller' value in getRouteDefaults(). By alias: the Route returns a 'type' value in getRouteDefaults(), which is then matched against the provided configuration from config.yml By class: requires the Route instance to implement RouteObjectInterface and return an object for getRouteContent(). The returned class type is then matched against the provided configuration from config.yml. Default: if configured, a default Controller will be used. Apart from this, the DynamicRouter is also capable of dynamically specifying which Template will be used, in a similar way to the one used to determine the Controller (in order of precedence): Explicit: The stored Route document itself can explicitly declare the target Template in getRouteDefaults(). By class: requires the Route instance to implement RouteObjectInterface and return an object for getRouteContent(). The returned class type is then matched against the provided configuration from config.yml. Here's an example of how to configure the above mentioned options:
2. https://github.com/doctrine/phpcr-odm

PDF brought to you by generated on July 18, 2013

Chapter 2: Routing | 11

Listing 2-4

1 # app/config/config.yml 2 cmf_routing: 3 dynamic: 4 generic_controller: cmf_content.controller:indexAction 5 controllers_by_type: 6 editablestatic: sandbox_main.controller:indexAction 7 controllers_by_class: 8 Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent: 9 cmf_content.controller::indexAction 10 templates_by_class: Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent: CmfContentBundle:StaticContent:index.html.twig

Notice that enabled: true is no longer present. It's only required if no other configuration parameter is provided. The router is automatically enabled as soon as you add any other configuration to the dynamic entry.
Internally, the routing component maps these configuration options to several RouteEnhancerInterface instances. The actual scope of these enhancers is much wider, and you can find more information about them in the Routing documentation page.

Linking a Route with a Model Instance


Depending on you application's logic, a requested URL may have an associated model instance from the database. Those Routes can implement the RouteObjectInterface, and optionally return a model instance, that will be automatically passed to the Controller as the $contentDocument variable, if declared as parameter. Note that a Route can implement the above mentioned interface but still not return any model instance, in which case no associated object will be provided. Furthermore, Routes that implement this interface can also have a custom Route name, instead of the default Symfony core compatible name, and can contain any characters. This allows you, for example, to set a path as the route name.

Redirects
You can build redirects by implementing the RedirectRouteInterface. If you are using the default PHPCR-ODM route provider, a ready to use implementation is provided in the RedirectRoute Document. It can redirect either to an absolute URI, to a named Route that can be generated by any Router in the chain or to another Route object known to the route provider. The actual redirection is handled by a specific Controller that can be configured as follows:
Listing 2-5

1 # app/config/config.yml 2 cmf_routing: 3 controllers_by_class: 4 Symfony\Cmf\Component\Routing\RedirectRouteInterface: cmf_routing.redirect_controller:redirectAction

PDF brought to you by generated on July 18, 2013

Chapter 2: Routing | 12

The actual configuration for this association exists as a service, not as part of a config.yml file. As discussed before, any of the approaches can be used.

URL Generation
Symfony CMF's Routing component uses the default Symfony2 components to handle route generation, so you can use the default methods for generating your URLs with a few added possibilities: Pass an implementation of either RouteObjectInterface or RouteAwareInterface as the name parameter Alternatively, supply an implementation of ContentRepositoryInterface and the id of the model instance as parameter content_id The route generation handles locales as well, see ContentAwareGenerator and locales.

The PHPCR-ODM Route Document


As mentioned above, you can use any route provider. The example in this section applies if you use the default PHPCR-ODM route provider (Symfony\Cmf\Bundle\RoutingBundle\Document\RouteProvider). All routes are located under a configured root path, for example /cms/routes. A new route can be created in PHP code as follows:
Listing 2-6

1 2 3 4 5 6 7 8 9 10 11 12 13 14

use Symfony\Cmf\Bundle\RoutingBundle\Document\Route; $route = new Route; $route->setParent($dm->find(null, '/routes')); $route->setName('projects');

// link a content to the route $content = new Content('my content'); $route->setRouteContent($content); // now define an id parameter; do not forget the leading slash if you want /projects/{id} and not /projects{id} $route->setVariablePattern('/{id}'); $route->setRequirement('id', '\d+'); $route->setDefault('id', 1);

This will give you a document that matches the URL /projects/<number> but also /projects as there is a default for the id parameter. Because you defined the {id} route parameter, your controller can expect an $id parameter. Additionally, because you called setRouteContent on the route, your controller can expect the $contentDocument parameter. The content could be used to define an intro section that is the same for each project or other shared data. If you don't need content, you can just not set it in the document. For more details, see the route document section in the RoutingBundle documentation.

PDF brought to you by generated on July 18, 2013

Chapter 2: Routing | 13

Integrating with SonataAdmin


If sonata-project/doctrine-phpcr-admin-bundle is added to the composer.json require section, the route documents are exposed in the SonataDoctrinePhpcrAdminBundle. For instructions on how to configure this Bundle see SonataDoctrinePhpcrAdminBundle. By default, use_sonata_admin is automatically set based on whether the SonataDoctrinePhpcrAdminBundle is available but you can explicitly disable it to not have it even if Sonata is enabled, or explicitly enable to get an error if Sonata becomes unavailable. There are a couple of configuration options for the admin. The content_basepath points to the root of your content documents.
Listing 2-7

1 # app/config/config.yml 2 cmf_routing: 3 # use true/false to force using / not using sonata admin 4 use_sonata_admin: auto 5 6 # used with Sonata Admin to manage content; defaults to cmf_core.content_basepath 7 content_basepath: ~

Terms Form Type


The Routing bundle defines a form type that can be used for classical "accept terms" checkboxes where you place URLs in the label. Simply specify cmf_routing_terms_form_type as the form type name and specify a label and an array with content_ids in the options:
Listing 2-8

1 $form->add('terms', 'cmf_routing_terms_form_type', array( 2 'label' => 'I have seen the <a href="%team%">Team</a> and <a href="%more%">More</a> 3 pages ...', 4 'content_ids' => array( 5 '%team%' => '/cms/content/static/team', 6 '%more%' => '/cms/content/static/more' 7 ), ));

The form type automatically generates the routes for the specified content and passes the routes to the trans twig helper for replacement in the label.

Further Notes
For more information on the Routing component of Symfony CMF, please refer to: Routing for most of the actual functionality implementation RoutingBundle for Symfony2 integration bundle for Routing Bundle Symfony2's Routing3 component page Handling Multi-Language Documents for some notes on multilingual routing

3. http://symfony.com/doc/current/components/routing/introduction.html

PDF brought to you by generated on July 18, 2013

Chapter 2: Routing | 14

Chapter 3

Content

Concept
At the heart of every CMS stands the content, an abstraction that the publishers can manipulate and that will later be presented to the page's users. The content's structure greatly depends on the project's needs, and it will have a significant impact on future development and use of the platform. Symfony CMF SE comes with the ContentBundle: a basic implementation of a content structure, including support for multiple languages and database storage of Routes.

Static Content
The StaticContent class declares the basic content's structure. Its structure is very similar to the ones used on Symfony2's ORM systems. Most of its fields are self explanatory and are what you would expect from a basic CMS: title, body, publishing information and a parent reference, to accommodate a tree-like hierarchy. It also includes a Block reference (more on that later). The two implemented interfaces reveal two of the features included in this implementation: RouteAwareInterface means that the content has associated Routes. PublishWorkflowInterface means that the content has publishing and unpublishing dates, which will be handled by Symfony CMF's core to determine whether or not to display the content from StaticContent.

Multilang Static Content


The MultilangStaticContent class extends StaticContent, offering the same functionality with multi language support. It specifies which fields are to be translated (title, body and tags) as well as a variable to declare the locale. It also specifies the translation strategy:

PDF brought to you by generated on July 18, 2013

Chapter 3: Content | 15

Listing 3-1

1 use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR; 2 3 /** 4 * @PHPCRODM\Document(translator="child", referenceable=true) 5 */

For information on the available translation strategies, refer to the Doctrine page regarding multilanguage support in PHPCR-ODM1.

Content Controller
A controller is also included that can render either of the above content document types. Its single action, indexAction, accepts a content instance and optionally the path of the template to be used for rendering. If no template path is provided, it uses a pre-configured default. The controller action also takes into account the document's publishing status and language (for MultilangStaticContent). Both the content instance and the optional template are provided to the controller by the DynamicRouter of the RoutingBundle. More information on this is available on the Routing system getting started page page.

Admin Support
The last component needed to handle the included content types is an administration panel. Symfony CMF can optionally support SonataDoctrinePHPCRAdminBundle2, a back office generation tool. For more information about it, please refer to the bundle's documentation section3. In ContentBundle, the required administration panels are already declared in the Admin folder and configured in Resources/config/admin.xml, and will automatically be loaded if you install the SonataDoctrinePHPCRAdminBundle (refer to Creating a CMS using CMF and Sonata for instructions on that).

Configuration
The ContentBundle also supports a set of optional configuration parameters. Refer to ContentBundle for the full configuration reference.

Final Thoughts
While this small bundle includes some vital components to a fully working CMS, it often will not provide all you need. The main idea behind it is to provide developers with a small and easy to understand starting point you can extend or use as inspiration to develop your own content types, Controllers and Admin panels.

1. http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/multilang.html 2. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle 3. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/tree/master/Resources/doc

PDF brought to you by generated on July 18, 2013

Chapter 3: Content | 16

Chapter 4

Menu

Concept
No CMS system is complete without a menu system that allows users to navigate between content pages and perform certain actions. While it usually maps the actual content tree structure, menus often have a logic of their own, include options not mapped by content or exist in multiple contexts with multiple options, thus making them a complex problem themselves.

Symfony CMF Menu System


Symfony CMF SE includes the MenuBundle, a tool that allow you to dynamically define your menus. It extends the KnpMenuBundle1, with a set of hierarchical, multi language menu elements, along with the tools to persist them in the chosen content store. It also includes the administration panel definitions and related services needed for integration with the SonataDoctrinePhpcrAdminBundle2.
The MenuBundle extends and greatly relies on the KnpMenuBundle3, so you should carefully read KnpMenuBundle's documentation4. For the rest of this page we assume you have done so and are familiar with concepts like Menu Providers and Menu Factories.

Usage
The MenuBundle uses KnpMenuBundle's default renderers and helpers to print out menus. You can refer to the respective documentation page5 for more information on the subject, but a basic call would be:
Listing 4-1

1. https://github.com/knplabs/KnpMenuBundle 2. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle 3. https://github.com/knplabs/KnpMenuBundle 4. https://github.com/KnpLabs/KnpMenuBundle/blob/master/Resources/doc/index.md 5. https://github.com/KnpLabs/KnpMenuBundle/blob/master/Resources/doc/index.md#rendering-menus

PDF brought to you by generated on July 18, 2013

Chapter 4: Menu | 17

1 {{ knp_menu_render('simple') }}

The provided menu name will be passed on to MenuProviderInterface implementation, which will use it to identify which menu you want rendered in this specific section.

The Provider
The core of the MenuBundle is PHPCRMenuProvider, a MenuProviderInterface implementation that's responsible for dynamically loading menus from a PHPCR database. The default provider service is configured with a menu_basepath to know where in the PHPCR tree it will find menus. The menu name is given when rendering the menu and must be a direct child of the menu base path. This allows the PHPCRMenuProvider to handle several menu hierarchies using a single storage mechanism. To give a concrete example, if we have the configuration as given below and render the menu simple, the menu root node must be stored at /cms/menu/simple.
Listing 4-2

1 cmf_menu: 2 menu_basepath: /cms/menu

If you need multiple menu roots, you can create further PHPCRMenuProvider instances and register them with KnpMenu - see the CMF MenuBundle DependencyInjection code for the details. The menu element fetched using this process is used as the menu root node, and its children will be loaded progressively as the full menu structure is rendered by the MenuFactory.

The Factory
The ContentAwareFactory is a FactoryInterface implementation, which generates the full MenuItem hierarchy from the provided MenuNode. The data generated this way is later used to generate the actual HTML representation of the menu. The included implementation focuses on generating MenuItem instances from NodeInterface instances, as this is usually the best approach to handle tree-like structures typically used by a CMS. Other approaches are implemented in the base classes, and their respective documentation pages can be found in KnpMenuBundle6's page. ContentAwareFactory is responsible for loading the full menu hierarchy and transforming the MenuNode instances from the root node it receives from the MenuProviderInterface implementation. It is also responsible for determining which (if any) menu item is currently being viewed by the user. It supports a voter mechanism to have custom code decide what menu item is the current item. KnpMenu already includes a specific factory targeted at Symfony2's Routing component, which this bundle extends, to add support for: Route instances stored in a database (refer to RoutingBundle's RouteProvider for more details on this) Route instances with associated content (more on this on respective RoutingBundle's section) As mentioned before, ContentAwareFactory is responsible for loading all the menu nodes from the provided root element. The actual loaded nodes can be of any class, even if it's different from the root's, but all must implement NodeInterface in order to be included in the generated menu.

6. https://github.com/knplabs/KnpMenuBundle

PDF brought to you by generated on July 18, 2013

Chapter 4: Menu | 18

The Menu Nodes


Also included in the MenuBundle are two menu node content types: MenuNode and MultilangMenuNode. If you have read the documentation page regarding Content, you'll find this implementation somewhat familiar. MenuNode implements the above mentioned NodeInterface, and holds the information regarding a single menu entry: a label and a uri, a children list, plus some attributes for the node and its children that will allow the rendering process to be customized. It also includes a Route field and two references to Contents. These are used to store an associated Route object, plus one (not two, despite the fact that two fields exist) Content element. The MenuNode can have a strong (integrity ensured) or weak (integrity not ensured) reference to the actual Content element it points to; it's up to you to choose which best fits your scenario. You can find more information on references on the Doctrine PHPCR documentation page7. MultilangMenuNode extends MenuNode with multilanguage support. It adds a locale field to identify which translation set it belongs to, plus label and uri fields marked as translated=true. This means they will differ between translations, unlike the other fields. MultilangMenuNode also specifies the strategy used to persist multiple translations:
Listing 4-3

1 /** 2 * @PHPCRODM\Document(translator="attribute") 3 */

For information on the available translation strategies, refer to the Doctrine page regarding Multi language support in PHPCR-ODM8

Admin Support
The MenuBundle also includes the administration panels and respective services needed for integration with the backend admin tool SonataDoctrinePhpcrAdminBundle The included administration panels are automatically available but need to be explicitly put on the dashboard if you want to use them. See Creating a CMS using CMF and Sonata for instructions on how to install SonataDoctrinePhpcrAdminBundle.

Configuration
This bundle is configurable using a set of parameters, but all of them are optional. You can go to the MenuBundle reference page for the full configuration options list and additional information.

Further Notes
For more information on the MenuBundle of Symfony CMF, please refer to: MenuBundle for advanced details and configuration reference KnpMenuBundle9 page for information on the bundle on which the MenuBundle relies KnpMenu10 page for information on the underlying library used by the KnpMenuBundle

7. http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/association-mapping.html#references 8. http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/multilang.html 9. https://github.com/knplabs/KnpMenuBundle 10. https://github.com/knplabs/KnpMenu

PDF brought to you by generated on July 18, 2013

Chapter 4: Menu | 19

Chapter 5

SimpleCMS

Concept
In the previous documentation pages all the basic components of Symfony CMF have been analysed: the Routing that allows you to associate URLs with your Content, which users can browse using a Menu. These three components complement each other but are independent: they work without each other, allowing you to choose which ones you want to use, extend or ignore. In some cases, however, you might just want a simple implementation that gathers all those capabilities into a ready-to-go package. For that purpose, the SimpleCMSBundle was created.

SimpleCMSBundle
The SimpleCMSBundle is implemented on top of most of the other Symfony CMF Bundles, combining them into a functional CMS. It is a simple solution, but you will find it very useful when you start implementing your own CMS using Symfony CMF. Whether you decide to extend or replace it, it's up to you, but in both cases, it's a good place to start developing your first CMS.

Page Document
Instead of separate documents for the content, routing and the menu system, the SimpleCMSBundle provides the Page document which provides all those roles in one class: It has properties for title and text body; It extends the Route class from the CMF RoutingBundle to work with the CMF router component, returning $this in getRouteContent(); It implements the RouteAwareInterface with getRoutes simply returning array($this) to allow the CMF router to generate the URL to a page; It implements NodeInterface, which means it can be used by CMF MenuBundle to generate a menu structure; It implements the PublishWorkflowInterface to be used with the publish workflow checker.

PDF brought to you by generated on July 18, 2013

Chapter 5: SimpleCMS | 20

Here's how that works in practice: The routing component receives a request that it matches to a Route instance loaded from persistent storage. That Route is a Page instance; The route enhancer asks the page for its content and will receive $this, putting the page into the request attributes; Other route enhancers determine the controller to use with this class and optionally the template to use (either a specific template stored with the page or one configured in the application configuration for the SimpleCmsBundle); The controller renders the page using the template, usually generating HTML content. The template might also render the menu, which will load all Pages and build a menu with them. This three-in-one approach is the key concept behind the bundle.

MultilangPage
As you would expect, a multilanguage version of Page is also included. MultilangPage defines a locale variable and which fields will be translated (title, label and body). It also includes getStaticPrefix() to handle the path prefix of the Page. This is part of the route handling mechanism, and will be discussed below. The MultilangPage class uses the attribute strategy for translation: several translations can coexist in the same database entry, and several translated versions of each field can be stored as different attributes in that same entry. As the routing is not separated from the content, it is not possible to create different routes for different languages. This is one of the main disadvantages of the SimpleCmsBundle.

Configuring the Content Class


SimpleCMSBundle will use Symfony\Cmf\Bundle\SimpleCmsBundle\Document\Page as the content class if multilanguage is not enabled (this is the default). If no other class is chosen, and multilanguage support is enabled, it will automatically switch to Symfony\Cmf\Bundle\SimpleCmsBundle\Document\MultilangPage. You can explicitly specify your content class and/or enable multilanguage support using the configuration parameters:
Listing 5-1

1 # app/config/config.yml 2 cmf_simple_cms: 3 # defaults to Symfony\Cmf\Bundle\SimpleCmsBundle\Document\Page or MultilangPage (see 4 above) 5 document_class: ~ 6 multilang: 7 # defaults to [] - declare your locales here to enable multilanguage locales: ~

SimpleCMSBundle in Detail
Now that you understand what the SimpleCMSBundle does, we'll detail how it does it. Several other components are part of this bundle that change the default behaviour of its dependencies.

Routing
The SimpleCMSBundle mostly relies on RoutingBundle and its set of configurable capabilities to meet its requirements. It declares an independent DynamicRouter service, with its own specific RouteProvider,
PDF brought to you by generated on July 18, 2013 Chapter 5: SimpleCMS | 21

NestedMatcher, Enhancers set and other useful services, all of them instances of the classes bundled with RoutingBundle. The only exception to this is RouteProvider: the SimpleCMSBundle has its own strategy to retrieve Route instances from persistent storage. This is related to the way Route instances are stored by RoutingBundle. By default, the path parameter will hold the prefixed full URI, including the locale identifier. This would mean an independent Route instance should exist for each translation of the same Content. However, as we've seen, MultilangPage```stores all translations in the same entry. So, to avoid duplication, the locale prefix is stripped from the URI prior to persistence, and SimpleCMSBundle includes ``MultilangRouteProvider, which is responsible for fetching Route instances taking that into account. When rendering the actual URL from Route, the locale prefix needs to be replaced, otherwise the resulting addresses would not specify the locale they refer to. To do so, MultilangPage uses the already mentioned getStaticPrefix() implementation. Example: An incoming request for contact would be prefixed with the /cms/simple basepath, and the storage would be queried for /cms/simple/contact/. However, in a multilanguage setup, the locale is prefixed to the URI, resulting in a query either for /cms/simple/en/contact/ or /cms/simple/de/ contact/, which would require two independent entries to exist for the same actual content. With the above mentioned approach, the locale is stripped from the URI prior to basepath prepending, resulting in a query for /cms/simple/contact/ in both cases.

Routes and Redirects


SimpleCMSBundle includes MultilangRoute and MultilangRedirectRoute. These are extensions to the Route and RedirectRoute found in RoutingBundle, but with the necessary changes to handle the prefix strategy discussed earlier.

Content Handling
Route instances are responsible for determining which Controller will handle the current request. Getting the Controller and Template shows how Symfony CMF SE can determine which Controller to use when rendering a certain content document, and the SimpleCMSBundle uses these mechanisms to do so.
Listing 5-2

1 # app/config/config.yml 2 cmf_simple_cms: 3 # defaults to cmf_content.controller:indexAction 4 generic_controller: ~

Unless you specify otherwise, the ContentController from SimpleCMSBundle is used for all Documents. The default configuration associates all document_class instances with this Controller, and specifies no default template. However, you can configure several controllers_by_class and templates_by_class rules, which will associate, respectively, Controller and templates to a specific Content type. Symfony CMF SE includes an example of both in its default configuration.
Listing 5-3

1 # app/config/config.yml 2 cmf_simple_cms: 3 routing: 4 templates_by_class: 5 Symfony\Cmf\Bundle\SimpleCmsBundle\Document\Page: 6 CmfSimpleCmsBundle:Page:index.html.twig 7 controllers_by_class: Symfony\Cmf\Bundle\RoutingBundle\Document\RedirectRoute: cmf_routing.redirect_controller:redirectAction

PDF brought to you by generated on July 18, 2013

Chapter 5: SimpleCMS | 22

These configuration parameters will be used to instantiate Route Enhancers. More information about them can be found in the Routing component documentation page. The specific example above determines that content instances of class Page will be rendered using the above template, if none other is explicitly provided by the associated Route (which, in this case, is Page itself). It also states that all content documents that instantiate RedirectRoute will be rendered using the specified Controller instead of the the default. Again, the actual Route can provided a controller that will take priority over this one. Both the template and the controller are part of SimpleCMSBundle.

Menu Generation
As mentioned before, Page implements NodeInterface, which means it can be used to generate a MenuItem that will, in turn, be rendered into HTML menus. To do so, the default MenuBundle mechanisms are used, only a custom basepath is provided to the PHPCRMenuProvider instance. This is defined in the SimpleCMSBundle configuration options, and used when handling content storage to support functionality as described in Menu documentation. This parameter is optional, and can be configured as follows:
Listing 5-4

1 # app/config/config.yml 2 cmf_simple_cms: 3 # defaults to auto; true/false can be used to force providing/not providing a menu 4 use_menu: ~ 5 6 # defaults to /cms/simple 7 basepath: ~

Admin Support
The SimpleCMSBundle also includes the administration panel and respective service needed for integration with SonataDoctrinePHPCRAdminBundle1, a backend editing bundle. For more information about it, please refer to the bundle's documentation section2. The included administration panels will automatically be loaded if you install the SonataDoctrinePHPCRAdminBundle (refer to Creating a CMS using CMF and Sonata for instructions on how to do so). You can change this behaviour with the following configuration option:
Listing 5-5

1 # app/config/config.yml 2 cmf_simple_cms: 3 # defaults to auto; true/false can be used to force using/not using SonataAdmin 4 use_sonata_admin: ~

Fixtures
The SimpleCMSBundle includes a support class for integration with DoctrineFixturesBundle3, aimed at making loading initial data easier. A working example is provided in Symfony CMF SE that illustrates how you can easily generate MultilangPage and MultilangMenuNode instances from YAML files.

1. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle 2. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/tree/master/Resources/doc 3. http://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html

PDF brought to you by generated on July 18, 2013

Chapter 5: SimpleCMS | 23

Configuration
This bundle is configurable using a set of parameters, but all of them are optional. You can go to the SimpleCmsBundle reference page for the full configuration options list and aditional information.

Further Notes
For more information on the SimpleCMSBundle, please refer to: SimpleCmsBundle for configuration reference and advanced details about the bundle. Routing for information about the routing component in which the SimpleCMSBundle is based on. Content for information about the base content bundle that the SimpleCMSBundle depends on. Menu for information about the menu system used by the SimpleCMSBundle.

PDF brought to you by generated on July 18, 2013

Chapter 5: SimpleCMS | 24

Chapter 6

Choosing a Storage Layer


When building a CMS, the choice of storage layer is one of the key decisions to take. Many factors must be considered, the good news is that with all the components and Bundles in the CMF, it takes extra care to provide the necessary extension points to ensure the CMF remains storage layer agnostic. The goal of this tutorial is to explain the considerations and why Symfony CMF suggest PHPCR1 and PHPCR-ODM2 as the ideal basis for a CMS. However, all components and Bundles can be integrated with other solutions with a fairly small amount of work.

Requirements for a CMS Storage Layer


At the most fundamental level a CMS is about storing, so the first requirement is that a CMS must provide means to store content with different properties. A CMS has very different storage needs than for example a system for processing orders. Do note however that it is entirely possible and very intended of the CMF initiative to enable developers to combine the CMF with a system for processing orders. So for example one could create a shopping solution using the CMF for storing the product catalog, while using another system for maintaining the inventory, customer data and orders. This leads to the second requirement, a CMS must provide means to reference content, both content stored inside the CMS, but also in other systems. The actual content in a CMS tends to be organized in a tree like structure, mimicking a file system. Note that content authors might want to use different structures for how to organize the content and how to organize other aspects like the menu and the routing. This leads to the third requirement, a CMS must provide means to represent the content as a tree structure. Furthermore a fourth requirement is that a CMS should allow maintaining several independent tree structures. In general data inside a CMS tends to be unstructured. So while several pages inside the CMS might be very similar, there is a good chance that there will be many permutations needing different extra fields, therefore a CMS must not enforce a singular schema for content. That being said, in order to better maintain the content structure and enabling UI layers from generically displaying content elements it is important to optionally be able to express rules that must be followed and that can also help attach

1. http://phpcr.github.com 2. http://www.doctrine-project.org/projects/phpcr-odm.html

PDF brought to you by generated on July 18, 2013

Chapter 6: Choosing a Storage Layer | 25

additional semantic meaning. So a CMS must provide means to optionally define a schema for content elements. This requirement actually also relates to another need, in that a CMS must make it easy for content authors to prepare a series of changes in a staging environment that then needs to go online in a single step. This means another requirement is that it is necessary that a CMS should support moving and exporting content between independent tree structures. Note that exporting can be useful also for backups. When making changes it would however also be useful to be able to version the change sets, so that they remain available for historical purposes, but also to be able to revert whenever needed. Therefore the next requirement is that a CMS should provide the ability to version content. As we live in a globalized world, websites need to provide content in multiple languages addressing different regions. However not all pieces of content need to be translated and others might only be eventually translated but until then the user should be presented the content in one of the available languages, so a CMS should provide the ability to store content in different languages, with optional fallback rules. As a CMS usually tends to store an increasing amount of content it will become necessary to provide some way for users to search the content even when the user has only a very fuzzy idea about the content they are looking for, leading to the requirement that a CMS must provide full text search capabilities, ideally leveraging both the contents tree structure and the data schema. Another popular need is limiting read and/or write access of content to specific users or groups. Ideally this solution would also integrate with the tree structure. So it would be useful if a CMS provides capabilities to define access controls that leverage the tree structure to quickly manage access for entire subtrees. Finally not all steps in the content authoring process will be done by the same person. As a matter of fact there might be multiple steps all of which might not even be done by a person. Instead some of the steps might even be executed by a machine. So for example a photographer might upload a new image, a content author might attach the photo to some text, then the system automatically generates thumbnails and web optimized renditions and finally an editor decides on the final publication. Therefore a CMS should provide capabilities to assist in the management of workflows.

Summary
Here is a summary of the above requirements. Note some of the requirements have a must, while others only have a should. Obviously depending on your use case you might prioritize features differently: a CMS must provide means to store content with different properties; a CMS must provide means to reference content; a CMS must provide means to represent the content as a tree structure; a CMS must provide full text search capabilities; a CMS must not enforce a singular schema for content; a CMS must provide means to optionally define a schema for content elements; a CMS should allow maintaining several independent tree structures; a CMS should support moving and exporting content between independent tree structures; a CMS should provide the ability to version content; a CMS should provide the ability to store content in different languages, with optional fallback rules; a CMS should provides capabilities to define access controls; a CMS should provide capabilities to assist in the management of workflows.

PDF brought to you by generated on July 18, 2013

Chapter 6: Choosing a Storage Layer | 26

RDBMS
Looking at the above requirements it becomes apparent that out the box an RDBMS is ill-suited to address the needs of a CMS. RDBMS were never intended to store tree structures of unstructured content. Really the only requirement RDBMS cover from the above list is the ability to store content, some way to reference content, keep multiple separate content structures and a basic level of access controls and triggers. This is not a failing of RDBMS in the sense that they were simply designed for a different use case: the ability to store, manipulate and aggregate structured data. This makes them ideal for storing inventory and orders. That is not to say that it is impossible to build a system on top of an RDBMS that addresses more or even all of the above topics. Some RDBMS natively support recursive queries, which can be useful for retrieving tree structures. Even if such native support is missing, there are algorithms like materialized path and nested sets that can enable efficient storage and retrieval of tree structures for different use cases. The point is however that these all require algorithms and code on top of an RDBMS which also tightly bind your business logic to a particular RDBMS and/or algorithm even if some of them can be abstracted. So again using an ORM one could create a pluggable system for managing tree structures with different algorithms which prevent binding the business logic of the CMS to a particular algorithm. However it should be said once more, that all Bundles and Components in the CMF are developed to enable any persistent storage API and we welcome contributions for adding implementations for other storage systems. So for example CMF RoutingBundle currently only provides Document classes for PHPCR ODM, but the interfaces defined in the Routing component are storage agnostic and we would accept a contribution to add Doctrine ORM support.

PHPCR
PHPCR3 essentially is a set of interfaces addressing most of the requirements from the above list. This means that PHPCR is totally storage agnostic in the sense that it is possible to really put any persistence solution behind PHPCR. So in the same way as an ORM can support different tree storage algorithms via some plugin, PHPCR aims to provide an API for the entire breath of CMS needs, therefore cleanly separating the entire business logic of your CMS from the persistence choice. As a matter of fact the only feature above not natively supported by PHPCR is support for translations. Thanks to the availability of several PHPCR implementations supporting various kinds of persistence choices, creating a CMS on top of PHPCR means that end users are enabled to pick and choose what works best for them, their available resources, their expertise and their scalability requirements. So for the simplest use cases there is for example a Doctrine DBAL based solution provided by the Jackalope4 PHPCR implementation that can use the SQLite RDBMS shipped with PHP itself. At the other end of the spectrum Jackalope also supports Jackrabbit5 which supports clustering and can efficiently handle data into the hundreds of gigabytes. By default Jackrabbit simply uses the file system for persistence, but it can also use an RDBMS. However future versions will support MongoDB and support for other NoSQL solutions like CouchDB or Cassandra is entirely possible. Again, switching the persistence solution would require no code changes as the business logic is only bound to the PHPCR interfaces. Please see Installing and Configuring Doctrine PHPCR-ODM for more details on the available PHPCR implementations and their requirements and how to setup Symfony2 with one of them.

3. http://phpcr.github.com 4. https://github.com/jackalope/jackalope 5. http://jackrabbit.apache.org

PDF brought to you by generated on July 18, 2013

Chapter 6: Choosing a Storage Layer | 27

PHPCR ODM
As mentioned above using PHPCR does not mean giving up on RDBMS. In many ways, PHPCR can be considered a specialized ORM solution for CMS. However while PHPCR works with so called nodes, in an ORM people expect to be able to map class instances to a persistence layer. This is exactly what PHPCR ODM provides. It follows the same interface classes as Doctrine ORM while also exposing all the additional capabilities of PHPCR, like trees and versioning. Furthermore, it also provides native support for translations, covering the only omission of PHPCR for the above mentioned requirements list of a CMS storage solution.

PDF brought to you by generated on July 18, 2013

Chapter 6: Choosing a Storage Layer | 28

Chapter 7

Installing and Configuring the CMF Core


The goal of this tutorial is to install the minimal CMF components ("core") with the minimum necessary configuration. From there, you can begin incorporating CMF functionality into your application as needed. This is aimed at experienced user who want to know all about the Symfony CMF details. If this is your first encounter with the Symfony CMF it would be a good idea to start with: Installing the Symfony CMF Standard Edition page for instructions on how to quickly install the CMF (recommended for development) Installing the CMF sandbox for instructions on how to install a demonstration sandbox.

Preconditions
Installation of Symfony21 (2.1.x) Installing and Configuring Doctrine PHPCR-ODM

Installation
Download the Bundles
Add the following to your composer.json file:
Listing 7-1

1 "minimum-stability": "dev", 2 "require": { 3 ... 4 "symfony-cmf/symfony-cmf": "1.0.*" 5 }

And then run:

1. http://symfony.com/doc/2.1/book/installation.html

PDF brought to you by generated on July 18, 2013

Chapter 7: Installing and Configuring the CMF Core | 29

Listing 7-2

1 $ php composer.phar update

Initialize bundles
Next, initialize the bundles in AppKernel.php by adding them to the registerBundles method:
Listing 7-3

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ...


new new new new new Symfony\Cmf\Bundle\RoutingBundle\CmfRoutingBundle(), Symfony\Cmf\Bundle\CoreBundle\CmfCoreBundle(), Symfony\Cmf\Bundle\MenuBundle\CmfMenuBundle(), Symfony\Cmf\Bundle\ContentBundle\CmfContentBundle(), Symfony\Cmf\Bundle\BlockBundle\CmfBlockBundle(),

// Dependencies of the CmfMenuBundle new Knp\Bundle\MenuBundle\KnpMenuBundle(), // Dependencies of the CmfBlockBundle new Sonata\BlockBundle\SonataBlockBundle(),
);

// ...
}

This also enables the PHPCR-ODM and related dependencies; setup instructions can be found in the dedicated documentation.

Configuration
To get your application running, very little configuration is needed.

Minimum Configuration
These steps are needed to ensure your AppKernel still runs. If you haven't done so already, make sure you have followed these steps from Installing and Configuring Doctrine PHPCR-ODM: Initialize DoctrinePHPCRBundle in app/AppKernel.php Ensure there is a doctrine_phpcr: section in app/config/config.yml Add the AnnotationRegistry::registerFile line to app/autoload.php Configure the BlockBundle in your config.yml:
Listing 7-4

PDF brought to you by generated on July 18, 2013

Chapter 7: Installing and Configuring the CMF Core | 30

1 # app/config/config.yml 2 sonata_block: 3 default_contexts: [cms]

Additional Configuration
Because most CMF components use the DynamicRouter from the RoutingBundle, which by default is not loaded, you will need to enable it as follows:
Listing 7-5

1 # app/config/config.yml 2 cmf_routing: 3 chain: 4 routers_by_id: 5 cmf_routing.dynamic_router: 200 6 router.default: 100 7 dynamic: 8 enabled: true

You might want to configure more on the dynamic router, i.e. to automatically choose controllers based on content. See RoutingBundle for details. For now this is the only configuration we need. Mastering the configuration of the different bundles will be handled in further tutorials. If you're looking for the configuration of a specific bundle take a look at the corresponding bundles entry.

PDF brought to you by generated on July 18, 2013

Chapter 7: Installing and Configuring the CMF Core | 31

Chapter 8

Installing and Configuring Doctrine PHPCRODM


The Symfony2 CMF needs somewhere to store the content. Many of the bundles provide documents and mappings for the PHP Content Repository - Object Document Mapper (PHPCR-ODM), and the documentation is currently based around using this. However, it should also be possible to use any other form of content storage, such as another ORM/ODM or MongoDB. The goal of this tutorial is to install and configure Doctrine PHPCR-ODM, ready for you to get started with the CMF. For more details see the full PHPCR-ODM documentation1. Some additional information can be found in the DoctrinePHPCRBundle reference, which for the most part mimics the standard DoctrineBundle2. Finally for information about PHPCR see the official PHPCR website3.
If you just want to use PHPCR but not the PHPCR-ODM, you can skip the step about registering annotations and the part of the configuration section starting with odm.

Requirements
Symfony2 (version 2.1 or newer) phpunit >= 3.6 (if you want to run the tests) When using jackalope-jackrabbit: Java, Apache Jackalope and libxml version >= 2.7.0 (due to a bug in libxml4) When using jackalope-doctrine-dbal with MySQL: MySQL >= 5.1.5 (as you need the xml function ExtractValue)

1. http://www.doctrine-project.org/projects/phpcr-odm.html 2. https://github.com/doctrine/DoctrineBundle 3. http://phpcr.github.com 4. http://bugs.php.net/bug.php?id=36501)

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 32

Installation
Choosing a Content Repository
The first thing to decide is what content repository to use. A content repository is essentially a database that will be responsible for storing all the content you want to persist. It provides an API that is optimized for the needs of a CMS (tree structures, references, versioning, full text search etc.). While every content repository can have very different requirements and performance characteristics, the API is the same for all of them. Furthermore, since the API defines an export/import format, you can always switch to a different content repository implementation later on. These are the available choices: Jackalope with Jackrabbit (Jackrabbit requires Java, it can persist into the file system, a database etc.) Jackalope with Doctrine DBAL (requires an RDBMS like MySQL, PostgreSQL or SQLite) Midgard (requires the midgard2 PHP extension and an RDBMS like MySQL, PostgreSQL or SQLite) The following documentation includes examples for all of the above options.
If you are just getting started with the CMF, it is best to choose a content repository based on a storage engine that you are already familiar with. For example, Jackalope with Doctrine DBAL will work with your existing RDBMS and does not require you to install Java or the midgard2 PHP extension. Once you have a working application it should be easy to switch to another option.

Download the Bundles


Add the following to your composer.json file, depending on your chosen content repository. Jackalope with Jackrabbit
Listing 8-1

1 "minimum-stability": "dev", 2 "require": { 3 ... 4 "jackalope/jackalope-jackrabbit": "1.0.*", 5 "doctrine/phpcr-bundle": "1.0.*", 6 "doctrine/phpcr-odm": "1.0.*" 7 }

Jackalope with Doctrine DBAL


Listing 8-2

1 "minimum-stability": "dev", 2 "require": { 3 ... 4 "jackalope/jackalope-doctrine-dbal": "dev-master", 5 "doctrine/phpcr-bundle": "1.0.*", 6 "doctrine/phpcr-odm": "1.0.*" 7 }

Midgard
Listing 8-3

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 33

1 "minimum-stability": "dev", 2 "require": { 3 ... 4 "midgard/phpcr": "dev-master", 5 "doctrine/phpcr-bundle": "1.0.*", 6 "doctrine/phpcr-odm": "1.0.*" 7 }

For all of the above, if you are also using Doctrine ORM, make sure to use "doctrine/orm": "2.3.*", otherwise composer can't resolve the dependencies as Doctrine PHPCR-ODM depends on the newer 2.3 Doctrine Commons. (Symfony2.1 standard edition uses 2.2.*.)

To install the above dependencies, run:


Listing 8-4

1 $ php composer.phar update

Register Annotations
PHPCR-ODM uses annotations and these need to be registered in your app/autoload.php file. Add the following line, immediately after the last AnnotationRegistry::registerFile line:
Listing 8-5

1 2 3 4 5

// app/autoload.php // ... AnnotationRegistry::registerFile(__DIR__.'/../vendor/doctrine/phpcr-odm/lib/Doctrine/ODM/ PHPCR/Mapping/Annotations/DoctrineAnnotations.php'); // ...

Initialize Bundles
Next, initialize the bundles in app/AppKernel.php by adding them to the registerBundle method:
Listing 8-6

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... // Doctrine PHPCR new Doctrine\Bundle\PHPCRBundle\DoctrinePHPCRBundle(),
);

// ...
}

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 34

Configuration
Next step is to configure the bundles.

PHPCR Session
Basic configuration for each content repository is shown below; add the appropriate lines to your app/ config/config.yml. More information on configuring this bundle can be found in the reference chapter DoctrinePHPCRBundle. The workspace, username and password parameters are for the PHPCR repository and should not be confused with possible database credentials. They come from your content repository setup. If you want to use a different workspace than default you have to create it first in your repository. If you want to use the PHPCR-ODM as well, please also see the next section. Jackalope with Jackrabbit
Listing 8-7

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 backend: 5 type: jackrabbit 6 url: http://localhost:8080/server/ 7 workspace: default 8 username: admin 9 password: admin 10 # odm configuration see below

Jackalope with Doctrine DBAL


Listing 8-8

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 backend: 5 type: doctrinedbal 6 connection: doctrine.dbal.default_connection 7 workspace: default 8 username: admin 9 password: admin 10 # odm configuration see below

Make sure you also configure the main doctrine: section for your chosen RDBMS. If you want to use a different than the default connection, configure it in the dbal section and specify it in the connection parameter. A typical example configuration is:
Listing 8-9

doctrine: dbal: driver: host: port: dbname: user: password: charset:

%database_driver% %database_host% %database_port% %database_name% %database_user% %database_password% UTF8

See `Databases and Doctrine`_ for more information.

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 35

Midgard
Listing 8-10

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 backend: 5 type: midgard2 6 db_type: MySQL 7 db_name: midgard2_test 8 db_host: "0.0.0.0" 9 db_port: 3306 10 db_username: "" 11 db_password: "" 12 db_init: true 13 blobdir: /tmp/cmf-blobs 14 workspace: default 15 username: admin 16 password: admin 17 # odm configuration see below

Doctrine PHPCR-ODM
Any of the above configurations will give you a valid PHPCR session. If you want to use the ObjectDocument manager, you need to configure it as well. The simplest is to set auto_mapping: true to make the PHPCR bundle recognize documents in the <Bundle>/Document folder and look for mappings in <Bundle>/Resources/config/doctrine/<Document>.phpcr.xml resp. ...yml. Otherwise you need to manually configure the mappings section. See the configuration reference of the PHPCR-ODM bundle for details.
Listing 8-11

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 # ... 5 odm: 6 auto_mapping: true

Setting up the Content Repository


Jackalope Jackrabbit These are the steps necessary to install Apache Jackrabbit: Make sure you have Java Virtual Machine installed on your box. If not, you can grab one from here: http://www.java.com/en/download/manual.jsp5 Download the latest version from the Jackrabbit Downloads page6 Run the server. Go to the folder where you downloaded the .jar file and launch it
Listing 8-12

1 $ java -jar jackrabbit-standalone-*.jar

Going to http://localhost:8080/ should now display a Apache Jackrabbit page. More information about running a Jackrabbit server7 can be found on the Jackalope wiki.
5. http://www.java.com/en/download/manual.jsp 6. http://jackrabbit.apache.org/downloads.html

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 36

Jackalope Doctrine DBAL Run the following commands to create the database and set up a default schema:
Listing 8-13

1 $ php app/console doctrine:database:create 2 $ php app/console doctrine:phpcr:init:dbal

For more information on how to configure Doctrine DBAL with Symfony2, see the "Databases and Doctrine8" and the explanations in the reference of the PHPCR-ODM bundle. Midgard Midgard is a C extension that implements the PHPCR API on top of a standard RDBMS. See the official Midgard PHPCR documentation9.

Registering System Node Types


PHPCR-ODM uses a `custom node type <>`_ to track meta information without interfering with your content. There is a command that makes it trivial to register this type and the PHPCR namespace, as well as all base paths of bundles:
Listing 8-14

1 $ php app/console doctrine:phpcr:repository:init

Using the ValidPhpcrOdm Constraint Validator


The bundle provides a ValidPhpcrOdm constraint validator you can use to check if your document Id or Nodename and Parent fields are correct:
Listing 8-15

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

<?php namespace Acme\DemoBundle\Document; use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM; use Doctrine\Bundle\PHPCRBundle\Validator\Constraints as Assert;

/** * @PHPCRODM\Document * @Assert\ValidPhpcrOdm */ class MyDocument { /** @PHPCRODM\Id(strategy="parent") */ protected $id; /** @PHPCRODM\Nodename */ protected $name; /** @PHPCRODM\ParentDocument */ protected $parent;

7. https://github.com/jackalope/jackalope/wiki/Running-a-jackrabbit-server 8. http://symfony.com/doc/current/book/doctrine.html 9. http://midgard-project.org/phpcr/

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 37

23 24 }

// ...

PDF brought to you by generated on July 18, 2013

Chapter 8: Installing and Configuring Doctrine PHPCR-ODM | 38

Chapter 9

Installing and Configuring Inline Editing


The goal of this tutorial is to install and configure the inline editing support. This provides a solution to easily integrate with VIE.js1 and create.js2 to provide inline editing based on RDFa3 output. For more information for now see the documentation of the CreateBundle

Installation
Download the Bundles
Add the following to your composer.json file:
Listing 9-1

1 2 3 4 5 6 7 8 9 10 11 12 13 14

"require": { ... "symfony-cmf/create-bundle": "1.0.*" }, "scripts": { "post-install-cmd": [ "Symfony\\Cmf\\Bundle\\CreateBundle\\Composer\\ScriptHandler::downloadCreate", ... ], "post-update-cmd": [ "Symfony\\Cmf\\Bundle\\CreateBundle\\Composer\\ScriptHandler::downloadCreate", ... ] },

And then run:


Listing 9-2

1. http://viejs.org 2. http://createjs.org 3. http://rdfa.info

PDF brought to you by generated on July 18, 2013

Chapter 9: Installing and Configuring Inline Editing | 39

1 $ php composer.phar update symfony-cmf/create-bundle

See Using CKEditor Instead if you want to use "CKEditor" instead of the default "hallo" editor.

Initialize Bundles
Next, initialize the bundles in the AppKernel by adding them to the registerBundle method:
Listing 9-3

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ...


new Symfony\Cmf\Bundle\CreateBundle\CmfCreateBundle(), new FOS\RestBundle\FOSRestBundle(), new JMS\SerializerBundle\JMSSerializerBundle($this), ); // ... }

Configuration
Next step is to configure the bundles. Basic configuration, add to your application configuration:
Listing 9-4

1 # app/config/config.yml 2 cmf_create: 3 phpcr_odm: true 4 map: 5 '<http://rdfs.org/sioc/ns#Post>': 6 'Symfony\Cmf\Bundle\MultilangContentBundle\Document\MultilangStaticContent' 7 image: 8 model_class: Symfony\Cmf\Bundle\CreateBundle\Document\Image controller_class: Symfony\Cmf\Bundle\CreateBundle\Controller\PHPCRImageController

If you have your own documents, add them to the mapping and place the RDFa mappings in Resources/ rdf-mappings either inside the app directory or inside any Bundle. The filename is the full class name including namespace with the backslashes \\ replaced by a dot ..

PDF brought to you by generated on July 18, 2013

Chapter 9: Installing and Configuring Inline Editing | 40

Chapter 10

Creating a CMS using CMF and Sonata


The goal of this tutorial is to create a simple content management system using the CMF as well as SonataAdminBundle1 and SonataDoctrinePhpcrAdminBundle.

Preconditions
installing-cmf-core Symfony SecurityBundle2 (required by the SonataAdminBundle default templates)

Installation
Download the Bundles
Add the following to your composer.json file:
Listing 10-1

1 "require": { 2 ... 3 "sonata-project/doctrine-phpcr-admin-bundle": "1.0.*", 4 }

And then run:


Listing 10-2

1 $ php composer.phar update

Initialize Bundles
Next, initialize the bundles in app/AppKernel.php by adding them to the registerBundle method:

1. https://github.com/sonata-project/SonataAdminBundle 2. http://symfony.com/doc/master/book/security.html

PDF brought to you by generated on July 18, 2013

Chapter 10: Creating a CMS using CMF and Sonata | 41

Listing 10-3

1 public function registerBundles() 2 { 3 $bundles = array( 4 // ... 5 6 // support for the admin 7 new Symfony\Cmf\Bundle\TreeBrowserBundle\CmfTreeBrowserBundle(), 8 new Sonata\jQueryBundle\SonatajQueryBundle(), 9 new Sonata\BlockBundle\SonataBlockBundle(), 10 new Sonata\AdminBundle\SonataAdminBundle(), 11 new Sonata\DoctrinePHPCRAdminBundle\SonataDoctrinePHPCRAdminBundle(), 12 new FOS\JsRoutingBundle\FOSJsRoutingBundle(), 13 ); 14 15 // ... 16 }

Configuration
Add the sonata bundles to your application configuration:
Listing 10-4

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

# app/config/config.yml sonata_block: default_contexts: [cms] blocks: sonata.admin.block.admin_list: contexts: [admin] sonata_admin_doctrine_phpcr.tree_block: settings: id: '/cms' contexts: [admin]
sonata_admin: templates: # default global templates ajax: SonataAdminBundle::ajax_layout.html.twig dashboard: blocks: # display a dashboard block - { position: right, type: sonata.admin.block.admin_list } - { position: left, type: sonata_admin_doctrine_phpcr.tree_block } sonata_doctrine_phpcr_admin: document_tree: Doctrine\ODM\PHPCR\Document\Generic: valid_children: - all Symfony\Cmf\Bundle\SimpleCmsBundle\Document\Page: ~ Symfony\Cmf\Bundle\RoutingBundle\Document\Route: valid_children: - Symfony\Cmf\Bundle\RoutingBundle\Document\Route - Symfony\Cmf\Bundle\RoutingBundle\Document\RedirectRoute Symfony\Cmf\Bundle\RoutingBundle\Document\RedirectRoute: valid_children: [] Symfony\Cmf\Bundle\MenuBundle\Document\MenuNode: valid_children:

PDF brought to you by generated on July 18, 2013

Chapter 10: Creating a CMS using CMF and Sonata | 42

36 37 38 39 40 41

- Symfony\Cmf\Bundle\MenuBundle\Document\MenuNode - Symfony\Cmf\Bundle\MenuBundle\Document\MultilangMenuNode Symfony\Cmf\Bundle\MenuBundle\Document\MultilangMenuNode: valid_children: - Symfony\Cmf\Bundle\MenuBundle\Document\MenuNode - Symfony\Cmf\Bundle\MenuBundle\Document\MultilangMenuNode

Add route in to your routing configuration:


Listing 10-5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

# app/config/routing.yml admin: resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml' prefix: /admin


sonata_admin: resource: . type: sonata_admin prefix: /admin doctrine_phpcr_admin_bundle_odm_browser: resource: "@SonataDoctrinePHPCRAdminBundle/Resources/config/routing/ phpcrodmbrowser.xml" fos_js_routing: resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml" cmf_tree: resource: . type: 'cmf_tree'

The FOSJsRoutingBundle is used to export sonata routes to javascript, to be used with the tree. All relevant routes have the expose option set. If you do custom routes that need to be used with the tree, you need to do that or configure the js routing bundle manually.

Sonata Assets
Listing 10-6

1 $ php app/console assets:install --symlink

Defining own Admin Classes


The CMF bundles come with predefined admin classes which will be activated automatically if Sonata PHPCR-ODM Admin is loaded. If you need to write different admins and do not want to load the defaults, you can deactivate the loading - see the documentation of the respective bundles. To load your own Admin service, you need to declare it as a service, tag with sonata.admin with manager_type="doctrine_phpcr". For the admin to work properly, you need to add a call for method setRouteBuilder to set it to the service sonata.admin.route.path_info_slashes, or your Admin will not work. The constructor expects three arguments, code, document class and controller name. You can pass an empty argument for the code, the document class must be the fully qualified class name of the document this admin is for and the third argument can be used to set a custom controller that does additional operations over the default sonata CRUD controller.
PDF brought to you by generated on July 18, 2013 Chapter 10: Creating a CMS using CMF and Sonata | 43

Listing 10-7

1 <service id="my_bundle.admin" class="%my_bundle.admin_class%"> 2 <tag name="sonata.admin" manager_type="doctrine_phpcr" group="dashboard.group_content" 3 label_catalogue="MyBundle" label="dashboard.label_my_admin" 4 label_translator_strategy="sonata.admin.label.strategy.underscore" /> 5 <argument/> 6 <argument>%my_bundle.document_class%</argument> 7 <argument>SonataAdminBundle:CRUD</argument> 8 9 <call method="setRouteBuilder"> 10 <argument type="service" id="sonata.admin.route.path_info_slashes" /> </call> </service>

Finally
Now Sonata is configured to work with the PHPCR you can access the dashboard using via /admin/ dashboard in your site.

Tree Problems
If you have not yet added anything to the content repository, the tree view will not load as it cannot find a root node. To fix this, load some data as fixtures by following "Using the BlockBundle and ContentBundle with PHPCR"

Further Reading
SonataDoctrinePhpcrAdminBundle handling-multilang-documents

PDF brought to you by generated on July 18, 2013

Chapter 10: Creating a CMS using CMF and Sonata | 44

Chapter 11

Using the BlockBundle and ContentBundle with PHPCR


The goal of this tutorial is to demonstrate how the CMF BlockBundle and ContentBundle can be used as stand-alone components, and to show how they fit into the PHPCR. This tutorial demonstrates the simplest possible usage, to get you up and running quickly. Once you are familiar with basic usage, the in-depth documentation of both bundles will help you to adapt these basic examples to serve more advanced use cases. We will begin with using only BlockBundle, with content blocks linked directly into the PHPCR. Next, we will introduce the ContentBundle to show how it can represent content pages containing blocks.
Although not a requirement for using BlockBundle or ContentBundle, this tutorial will also make use of DoctrineFixturesBundle1. This is because it provides an easy way to load in some test content.

Preconditions
Installation of Symfony22 (2.1.x) Installing and Configuring Doctrine PHPCR-ODM
This tutorial is based on using PHPCR-ODM set up with Jackalope, Doctrine DBAL and a MySQL database. It should be easy to adapt this to work with one of the other PHPCR options documented in Installing and Configuring Doctrine PHPCR-ODM.

1. http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html 2. http://symfony.com/doc/2.1/book/installation.html

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 45

Create and Configure the Database


You can use an existing database, or create one now to help you follow this tutorial. For a new database, run these commands in MySQL:
Listing 11-1

1 CREATE DATABASE symfony DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; 2 CREATE USER 'symfony'@'localhost' IDENTIFIED BY 'UseABetterPassword'; 3 GRANT ALL ON symfony.* TO 'symfony'@'localhost';

Your parameters.yml file needs to match the above, for example:


Listing 11-2

1 # app/config/parameters.yml 2 parameters: 3 database_driver: pdo_mysql 4 database_host: localhost 5 database_port: ~ 6 database_name: symfony 7 database_user: symfony 8 database_password: UseABetterPassword

Configure the Doctrine PHPCR Component


If you have followed Installing and Configuring Doctrine PHPCR-ODM, you can skip this section.

You need to install the PHPCR-ODM components. Add the following to your composer.json file:
Listing 11-3

1 "require": { 2 ... 3 "jackalope/jackalope-jackrabbit": "1.0.*", 4 "jackalope/jackalope-doctrine-dbal": "dev-master", 5 "doctrine/phpcr-bundle": "1.0.*", 6 "doctrine/phpcr-odm": "1.0.*" 7 }

To install the above, run:


Listing 11-4

1 $ php composer.phar update

In your config.yml file, add following configuration for doctrine_phpcr:


Listing 11-5

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 backend: 5 type: doctrinedbal 6 connection: doctrine.dbal.default_connection 7 workspace: default 8 odm: 9 auto_mapping: true

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 46

Add the following line to the registerBundles() method of the AppKernel:


Listing 11-6

1 2 3 4 5 6 7 8 9 10 11 12

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... new Doctrine\Bundle\PHPCRBundle\DoctrinePHPCRBundle(), ); // ...
}

Add the following line to your AnnotationRegistry::registerFile line:


Listing 11-7

autoload.php

file,

immediately

after

the

last

1 2 3 4 5

// app/autoload.php // ... AnnotationRegistry::registerFile(__DIR__.'/../vendor/doctrine/phpcr-odm/lib/Doctrine/ODM/ PHPCR/Mapping/Annotations/DoctrineAnnotations.php'); // ...

Create the database schema and register the PHPCR node types using the following console commands:
Listing 11-8

1 $ php app/console doctrine:phpcr:init:dbal 2 $ php app/console doctrine:phpcr:repository:init

Now you should have a number of tables in your MySQL database with the phpcr_ prefix.

Install the needed Symfony CMF Components


To install the BlockBundle, run:
Listing 11-9

1 $ php composer.phar require symfony-cmf/block-bundle:master

Add the following lines to AppKernel:


Listing 11-10

1 2 3 4 5 6 7 8 9 10 11 12 13

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... new Sonata\BlockBundle\SonataBlockBundle(), new Symfony\Cmf\Bundle\BlockBundle\CmfBlockBundle(), ); // ...
}

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 47

SonataBlockBundle is a dependency of the CMF BlockBundle and needs to be configured. Add the following to your config.yml:
Listing 11-11

1 # app/config/config.yml 2 sonata_block: 3 default_contexts: [cms]

Install DoctrineFixturesBundle
As mentioned at the start, this is not a requirement for BlockBundle or ContentBundle; nevertheless it is a good way to manage example or default content.

To install the DoctrineFixturesBundle, run:


Listing 11-12

1 $ php composer.phar require doctrine/doctrine-fixtures-bundle:dev-master

Add the following line to the registerBundles() method of the AppKernel:


Listing 11-13

1 2 3 4 5 6 7 8 9 10 11 12

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(), ); // ...
}

Loading Fixtures
Based on the DoctrineFixturesBundle documentation3, you will need to create a fixtures class. To start with, create a DataFixtures directory inside your own bundle (e.g. "MainBundle"), and inside there, create a directory named PHPCR. As you follow the examples further below, the DoctrineFixturesBundle will automatically load the fixtures classes placed here. Within a fixtures loader, an example of creating a content block might look like this:
Listing 11-14

1 2 3 4 5 6 7

$myBlock = new SimpleBlock(); $myBlock->setParentDocument($parentPage); $myBlock->setName('sidebarBlock'); $myBlock->setTitle('My first block'); $myBlock->setContent('Hello block world!'); $documentManager->persist($myBlock);

3. http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 48

The above on its own will not be enough however, because there is no parent ($parentPage) to link the blocks to. There are several possible options that you can use as the parent: Link the blocks directly to the root document (not shown) Create a document from the PHPCR bundle (shown below using the Generic document type) Create a document from the CMF ContentBundle (shown below using StaticContent document type)

Using the PHPCR


To store a CMF block directly in the PHPCR, create the following class inside your DataFixtures/PHPCR directory:
Listing 11-15

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

<?php // src/Acme/MainBundle/DataFixtures/PHPCR/LoadBlockWithPhpcrParent.php namespace Acme\MainBundle\DataFixtures\ORM; use use use use use use Doctrine\Common\DataFixtures\AbstractFixture; Doctrine\Common\Persistence\ObjectManager; Doctrine\ODM\PHPCR\Document\Generic; Symfony\Component\DependencyInjection\ContainerAwareInterface; Symfony\Component\DependencyInjection\ContainerInterface; Symfony\Cmf\Bundle\BlockBundle\Document\SimpleBlock;

class LoadBlockWithPhpcrParent extends AbstractFixture implements ContainerAwareInterface { public function load(ObjectManager $manager) { // Get the root document from the PHPCR $rootDocument = $manager->find(null, '/');

// Create a generic PHPCR document under the root, to use as a kind of category for the blocks $document = new Generic(); $document->setParent($rootDocument); $document->setNodename('blocks'); $manager->persist($document); // Create a new SimpleBlock (see http://symfony.com/doc/master/cmf/bundles/ block.html#block-types) $myBlock = new SimpleBlock(); $myBlock->setParentDocument($document); $myBlock->setName('testBlock'); $myBlock->setTitle('CMF BlockBundle only'); $myBlock->setContent('Block from CMF BlockBundle, parent from the PHPCR (Generic document).'); $manager->persist($myBlock); // Commit $document and $block to the database $manager->flush();
} public function setContainer(ContainerInterface $container = null) { $this->container = $container; } }

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 49

This class loads an example content block using the CMF BlockBundle (without needing any other CMF bundle). To ensure the block has a parent in the repository, the loader also creates a Generic document named 'blocks' within the PHPCR. Now load the fixtures using the console:
Listing 11-16

1 $ php app/console doctrine:phpcr:fixtures:load

The content in your database should now look something like this:
Listing 11-17

1 SELECT path, parent, local_name FROM phpcr_nodes;

path / /blocks /blocks/testBlock

parent / / blocks

local_name blocks testBlock

Using the CMF ContentBundle


Follow this example to use both the CMF Block and Content components together. The ContentBundle is best used together with the RoutingBundle. Add the following to composer.json:
Listing 11-18

1 "require": { 2 ... 3 "symfony-cmf/content-bundle": "dev-master", 4 "symfony-cmf/routing-bundle": "dev-master" 5 }

Install as before:
Listing 11-19

1 $ php composer.phar update

Add the following line to AppKernel:


Listing 11-20

1 2 3 4 5 6 7 8 9 10 11 12

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... new Symfony\Cmf\Bundle\ContentBundle\CmfContentBundle(), ); // ...
}

Now you should have everything needed to load a sample content page with a sample block, so create the LoadBlockWithCmfParent.php class:
Listing 11-21

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 50

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

<?php // src/Acme/Bundle/MainBundle/DataFixtures/PHPCR/LoadBlockWithCmfParent.php namespace Acme\MainBundle\DataFixtures\PHPCR; use use use use use use use Doctrine\Common\DataFixtures\AbstractFixture; Doctrine\Common\Persistence\ObjectManager; Symfony\Component\DependencyInjection\ContainerAwareInterface; Symfony\Component\DependencyInjection\ContainerInterface; PHPCR\Util\NodeHelper; Symfony\Cmf\Bundle\BlockBundle\Document\SimpleBlock; Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent;

class LoadBlockWithCmfParent extends AbstractFixture implements ContainerAwareInterface { public function load(ObjectManager $manager) { // Get the base path name to use from the configuration $session = $manager->getPhpcrSession(); $basepath = $this->container->getParameter('cmf_content.static_basepath');

// Create the path in the repository NodeHelper::createPath($session, $basepath); // Create a new document using StaticContent from the CMF ContentBundle $document = new StaticContent(); $document->setPath($basepath . '/blocks'); $manager->persist($document); // Create a new SimpleBlock (see http://symfony.com/doc/master/cmf/bundles/ block.html#block-types) $myBlock = new SimpleBlock(); $myBlock->setParentDocument($document); $myBlock->setName('testBlock'); $myBlock->setTitle('CMF BlockBundle and ContentBundle'); $myBlock->setContent('Block from CMF BlockBundle, parent from CMF ContentBundle (StaticContent).'); $manager->persist($myBlock); // Commit $document and $block to the database $manager->flush();
} public function setContainer(ContainerInterface $container = null) { $this->container = $container; } }

This class creates an example content page using the CMF ContentBundle. It then loads our example block as before, using the new content page as its parent. By default, the base path for the content is /cms/content/static. To show how it can be configured to any path, add the following, optional entry to your config.yml:
Listing 11-22

1 # app/config/config.yml 2 cmf_content: 3 static_basepath: /content

Now it should be possible to load in the above fixtures:


PDF brought to you by generated on July 18, 2013 Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 51

Listing 11-23

1 $ php app/console doctrine:phpcr:fixtures:load

All being well, the content in your database should look something like this (if you also followed the LoadBlockWithPhpcrParent example, you should still have two /blocks entries as well):
Listing 11-24

1 SELECT path, parent, local_name FROM phpcr_nodes;

path / /content /content/blocks /content/blocks/testBlock

parent / /content /content/blocks

local_name content blocks testBlock

Rendering the Blocks


This is handled by the Sonata BlockBundle. sonata_block_render is already registered as a Twig extension by including SonataBlockBundle in AppKernel.php. Therefore, you can render any block within any template by referring to its path. The following code shows the rendering of both testBlock instances from the examples above. If you only followed one of the examples, make sure to only include that block:
Listing 11-25

1 2 3 4 5 6 7 8 9 10 11

{# src/Acme/Bundle/MainBundle/resources/views/Default/index.html.twig #} {# include this if you followed the BlockBundle with PHPCR example #} {{ sonata_block_render({ 'name': '/blocks/testBlock' }) }} {# include this if you followed the BlockBundle with ContentBundle example #} {{ sonata_block_render({ 'name': '/content/blocks/testBlock' }) }}

Now your index page should show the following (assuming you followed both examples):
Listing 11-26

1 2 3 4 5

CMF BlockBundle only Block from CMF BlockBundle, parent from the PHPCR (Generic document). CMF BlockBundle and ContentBundle Block from CMF BlockBundle, parent from CMF ContentBundle (StaticContent).

This happens when a block is rendered, see the BlockBundle for more details: a document is loaded based on the name if caching is configured, the cache is checked and content is returned if found each block document also has a block service, the execute method of it is called: you can put here logic like in a controller it calls a template the result is a Response object
PDF brought to you by generated on July 18, 2013 Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 52

A block can also be configured using settings, this allows you to create more advanced blocks and reuse it. The default settings are configured in the block service and can be altered in the bundle configuration, the twig helper and the block document. An example is an rss reader block, the url and title are stored in the settings of the block document, the maximum amount of items to display is specified when calling sonata_block_render.

Embedding Blocks in WYSIWYG Content


The CmfBlockBundle provides a twig filter cmf_embed_blocks that looks through the content and looks for special tags to render blocks. To use the tag, you need to apply the cmf_embed_blocks filter to your output. If you can, render your blocks directly in the template. This feature is only a cheap solution for web editors to place blocks anywhere in their HTML content. A better solution to build composed pages is to build it from blocks (there might be a CMF bundle at some point for this).
Listing 11-27

1 {# template.twig.html #} 2 {{ page.content|cmf_embed_blocks }}

When you apply the filter, your users can use this tag to embed a block in their HTML content:
Listing 11-28

1 <span>%embed-block:"/absolute/path/to/block"%</span> 2 3 <span>%embed-block:"local-block"%</span>

The path to the block is either absolute or relative to the current main content. The actual path to the block must be enclosed with double quotes ". But the prefix and postfix are configurable. The default prefix is <span>%embed-block: and the default postfix is %</span>. Say you want to write %%%block:"/ absolute/path"%%% and no <span> tag then you do:
Listing 11-29

# app/config/config.yml cmf_block: twig: cmf_embed_blocks: prefix: %%%block: postfix: %%%

Currently there is no limitation built into this feature. Only enable it on content for which you are sure only trusted users may edit it. Restrictions about what block can be where that are built into an admin interface are not respected here.

Next Steps
You should now be ready to use the BlockBundle and/or the ContentBundle in your application, or to explore the other available CMF bundles. See the BlockBundle and ContentBundle documentation to learn about more advanced usage of these bundles To see a better way of loading fixtures, look at the fixtures in the CMF Sandbox4

4. https://github.com/symfony-cmf/cmf-sandbox/tree/master/src/Sandbox/MainBundle/DataFixtures/PHPCR

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 53

Take a look at the PHPCR Tutorial5 for a better understanding of the underlying content repository

Troubleshooting
If you run into problems, it might be easiest to start with a fresh Symfony2 installation. You can also try running and modifying the code in the external CMF Block Sandbox6 working example.

Doctrine configuration
If you started with the standard Symfony2 distribution (version 2.1.x), this should already be configured correctly in your config.yml file. If not, try using the following section:
Listing 11-30

1 # app/config/config.yml 2 doctrine: 3 dbal: 4 driver: "%database_driver%" 5 host: "%database_host%" 6 port: "%database_port%" 7 dbname: "%database_name%" 8 user: "%database_user%" 9 password: "%database_password%" 10 charset: UTF8 11 orm: 12 auto_generate_proxy_classes: "%kernel.debug%" 13 auto_mapping: true

"No commands defined" when loading fixtures


Listing 11-31

1 [InvalidArgumentException] 2 There are no commands defined in the "doctrine:phpcr:fixtures" namespace.

Make sure AppKernel.php contains the following lines:


Listing 11-32

1 new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(), 2 new Doctrine\Bundle\PHPCRBundle\DoctrinePHPCRBundle(),

"You did not configure a session"


Listing 11-33

1 [InvalidArgumentException] 2 You did not configure a session for the document managers

Make sure you have the following in your config file:


Listing 11-34

1 # app/config.yml 2 doctrine_phpcr: 3 session:

5. https://github.com/phpcr/phpcr-docs/blob/master/tutorial/Tutorial.md 6. https://github.com/fazy/cmf-block-sandbox

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 54

4 5 6 7 8 9

backend: type: doctrinedbal connection: doctrine.dbal.default_connection workspace: default odm: auto_mapping: true

"Annotation does not exist"


Listing 11-35

1 [Doctrine\Common\Annotations\AnnotationException] 2 [Semantical Error] The annotation "@Doctrine\ODM\PHPCR\Mapping\Annotations\Document" in class Doctrine\ODM\PHPCR\Document\Generic does not exist, or could not be auto-loaded.

Make sure you add this line to your AnnotationRegistry::registerLoader line):


Listing 11-36

app/autoload.php

(immediately

after

the

1 AnnotationRegistry::registerFile(__DIR__.'/../vendor/doctrine/phpcr-odm/lib/Doctrine/ODM/ PHPCR/Mapping/Annotations/DoctrineAnnotations.php');

SimpleBlock class not found


Listing 11-37

1 [Doctrine\Common\Persistence\Mapping\MappingException] 2 The class 'Symfony\Cmf\Bundle\BlockBundle\Document\SimpleBlock' was not found in the chain configured namespaces Doctrine\ODM\PHPCR\Document, Sonata\UserBundle\Document, FOS\UserBundle\Document

Make sure the CMF BlockBundle is installed and loaded in app/AppKernel.php:


Listing 11-38

1 new Symfony\Cmf\Bundle\BlockBundle\CmfBlockBundle(),

RouteAwareInterface not found


Listing 11-39

1 Fatal error: Interface 'Symfony\Cmf\Component\Routing\RouteAwareInterface' not found in /var/www/your-site/vendor/symfony-cmf/content-bundle/Symfony/Cmf/Bundle/ContentBundle/ Document/StaticContent.php on line 15

If you are using ContentBundle, make sure you have also installed the RoutingBundle:
Listing 11-40

1 $ php composer.phar require symfony-cmf/routing-bundle:dev-master

PDF brought to you by generated on July 18, 2013

Chapter 11: Using the BlockBundle and ContentBundle with PHPCR | 55

Chapter 12

Handling Multi-Language Documents


The goal of the tutorial is to describe all the steps that are needed to be taken to use multi-language documents as clearly as possible.

Please read this first


You need to understand how to use PHPCR-ODM. You find an introduction in Documentation on the PHPCR-ODM Doctrine bundle

Lunetics LocaleBundle
The CMF recommends to rely on the LuneticsLocaleBundle1 to handle initial locale selection when a user first visits the site, and to provide a locale switcher. To install the bundle, require it in your project with:
Listing 12-1

1 $ php composer.phar require lunetics/locale-bundle

and then instantiate Lunetics\LocaleBundle\LuneticsLocaleBundle in your AppKernel.php. You also need the intl php extension installed and enabled. (Otherwise composer will tell you it can't find ext-intl.) If you get issues that some locales can not be loaded, have a look at this discussion about ICU2. Then configure it in the main application configuration file. As there are several CMF bundles wanting the list of allowed locales, we recommend putting them into a parameter %locales%, see the cmf-sandbox config.yml file3 for an example.

1. https://github.com/lunetics/LocaleBundle/ 2. https://github.com/symfony/symfony/issues/5279#issuecomment-11710480 3. https://github.com/symfony-cmf/cmf-sandbox/blob/master/app/config/config.yml

PDF brought to you by generated on July 18, 2013

Chapter 12: Handling Multi-Language Documents | 56

Whenever you do a sub-request, for example to call a controller from a twig template, do not forget to pass the app.request.locale along or you will lose the request locale and fall back to the default. See for example the action to include the create.js javascript files in the create.js reference.

PHPCR-ODM multi-language Documents


You can mark any properties as being translatable and have the document manager store and load the correct language for you. Note that translation always happens on a document level, not on the individual translatable fields.
Listing 12-2

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

<?php

/** * @PHPCRODM\Document(translator="attribute") */ class MyPersistentClass { /** * Translated property * @String(translated=true) */ private $topic; // ...
}

Read more about multi-language documents in the PHPCR-ODM documentation on multi-language4 and see Translation Configuration to configure PHPCR-ODM correctly. Most of the CMF bundles provide multi-language documents, for example MultilangStaticContent, MultilangMenuNode or MultilangSimpleBlock. The routing is different, as explained in the next section.

Routing
The DynamicRouter uses a route source to get routes that could match a request. The concept of the default PHPCR-ODM source is to map the request URL onto an id, which in PHPCR terms is the repository path to a node. This allows for a very efficient lookup without needing a full search over the repository. But a PHPCR node has exactly one path, therefore we need a separate route document for each locale. The cool thing with this is that we can localize the URL for each language. Simply create one route document per locale, and set a default value for _locale to point to the locale of that route. As all routes point to the same content, the route generator can handle picking the correct route for you when you generate the route from the content. See also ContentAwareGenerator and locales.

Sonata PHPCR-ODM Admin


This section explains how to make Sonata Admin handle multi-language documents. You should already have set up Sonata PHPCR-ODM Admin and understand how it works, see Creating a CMS using the CMF and Sonata.
4. http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/multilang.html

PDF brought to you by generated on July 18, 2013

Chapter 12: Handling Multi-Language Documents | 57

The following assumes that you installed the LuneticsLocaleBundle as explained above. If you want to use something else or write your own locale handling, first think if it would not make sense to give the Lunetics bundle a try. If you are still convinced you will need to adapt the following template examples to your way of building a locale switcher.

The first step is to configure sonata admin. We are going to place the LuneticsLocaleBundle language switcher in the topnav bar. To do this we need to configure the template for the user_block as shown below:
Listing 12-3

1 # app/config/config.yml 2 sonata_admin: 3 # ... 4 templates: 5 user_block: AcmeCoreBundle:Admin:admin_topnav.html.twig

And the template looks like this:


Listing 12-4

1 2 3 4 5 6 7

{# src/Acme/CoreBundle/Resources/views/Admin/admin_topnav.html.twig #} {% extends 'SonataAdminBundle:Core:user_block.html.twig' %}


{% block user_block %} {{ locale_switcher(null, null, 'AcmeCoreBundle:Admin:switcher_links.html.twig') }} {{ parent() }} {% endblock %}

You need to tell the locale_switcher to use a custom template to display the links, which looks like this:
Listing 12-5

1 2 3 4 5

{# src/Acme/CoreBundle/Resources/views/Admin/switcher_links.html.twig #} Switch to : {% for locale in locales %} {% if loop.index > 1 %} | {% endif %}<a href="{{ locale.link }}" title="{{ locale.locale_target_language }}">{{ locale.locale_target_language }}</a> {% endfor %}

Now what is left to do is to update the sonata routes to become locale aware:
Listing 12-6

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

# app/config/routing.yml
admin_dashboard: pattern: /{_locale}/admin/ defaults: _controller: FrameworkBundle:Redirect:redirect route: sonata_admin_dashboard permanent: true # this for 301 admin: resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml' prefix: /{_locale}/admin sonata_admin: resource: . type: sonata_admin prefix: /{_locale}/admin

PDF brought to you by generated on July 18, 2013

Chapter 12: Handling Multi-Language Documents | 58

19 # redirect routes for the non-locale routes 20 admin_without_locale: 21 pattern: /admin 22 defaults: 23 _controller: FrameworkBundle:Redirect:redirect 24 route: sonata_admin_dashboard 25 permanent: true # this for 301 26 27 admin_dashboard_without_locale: 28 pattern: /admin/dashboard 29 defaults: 30 _controller: FrameworkBundle:Redirect:redirect 31 route: sonata_admin_dashboard 32 permanent: true # this for 301

When we now reload the admin dashboard, the url should be prefixed with our default locale, for example /de/admin/dashboard. When clicking on the language switcher the page reloads and displays the correct content for the requested language. The provided sonata admin classes map the locale field of the multi-language documents to the form. You need to do the same in your admins, in order to create new translations. Otherwise the language fallback of PHPCR-ODM will make you update the original language, even when you request a different locale. With the mapped locale field, the editor can chose if he needs to create a new language version or updates the loaded one.

Frontend Editing and multi-language


When using the CreateBundle, you do not need to do anything at all to get multi-language support. PHPCR-ODM will deliver the document in the requested language, and the callback URL is generated in the request locale, leading to save the edited document in the same language as it was loaded.
If a translation is missing, language fallback kicks in, both when viewing the page but also when saving the changes, so you always update the current locale. It would make sense to offer the user the choice whether he wants to create a new translation or update the existing one. There is this issue5 in the CreateBundle issue tracker.

5. https://github.com/symfony-cmf/CreateBundle/issues/39

PDF brought to you by generated on July 18, 2013

Chapter 12: Handling Multi-Language Documents | 59

Chapter 13

The BlockBundle
The BlockBundle1 provides integration with SonataBlockBundle. It assists you in managing fragments of contents, so-called blocks. What the BlockBundle does is similar to what Twig does, but for blocks that are persisted in a DB. Thus, the blocks can be made editable for an editor. Also the BlockBundle provides the logic to determine which block should be rendered on which pages. The BlockBundle does not provide an editing functionality for blocks itself. However, you can find examples on how making blocks editable in the Symfony CMF Sandbox2.

Dependencies
This bundle is based on the SonataBlockBundle3.

Configuration

1. https://github.com/symfony-cmf/BlockBundle#readme 2. https://github.com/symfony-cmf/cmf-sandbox 3. https://github.com/sonata-project/SonataBlockBundle

PDF brought to you by generated on July 18, 2013

Chapter 13: The BlockBundle | 60

Updated SonataBlockBundle defaults


Prepended Configuration The BlockBundle automatically changes some defaults and adds configuration to the SonataBlockBundle4 to make it work nicely. This is done using the prepended configuration5 option of Symfony available since version 2.2. See DependencyInjection\CmfBlockExtension::prepend. Updated defaults: templates.block_base the cmf base template wraps the block output in a div and dashifies the PHPCR path as id; The base template is kept compatible with the Sonata base template for non-cmf blocks; RssBlock configuration adds the default RssBlock settings.
Settings are only prepended, define the settings explicitly inside the app/config/ config.yml to override them.

The configuration key for this bundle is cmf_block:


Listing 13-1

1 # app/config/config.yml 2 cmf_block: 3 manager_name: default

The default settings of a block are defined in the block service. If you use a 3rd party block you might want to alter these for your application. Use the sonata_block key for this. You can define default settings for a block service type or more specific for a block class. The later is usefull as a block service can be used by multiple block classes and sometimes you only want specific settings for one of the block classes.
Listing 13-2

1 # app/config/config.yml 2 sonata_block: 3 blocks: 4 acme_main.block.news: 5 settings: 6 maxItems: 3 7 blocks_by_class: 8 Symfony\Cmf\Bundle\BlockBundle\Document\RssBlock: 9 settings: 10 maxItems: 3

Block Document
Before you can render a block, you need to create a data object representing your block in the repository. You can do so with the following code snippet:
Listing 13-3

4. https://github.com/sonata-project/SonataBlockBundle 5. http://symfony.com/doc/current/components/dependency_injection/compilation.html#prepending-configuration-passed-to-the-extension

PDF brought to you by generated on July 18, 2013

Chapter 13: The BlockBundle | 61

1 2 3 4 5 6 7 8 9 10 11

use Symfony\Cmf\Bundle\BlockBundle\Document\SimpleBlock;

// ...
$myBlock = new SimpleBlock(); $myBlock->setParentDocument($parentDocument); $myBlock->setName('sidebarBlock'); $myBlock->setTitle('My first block'); $myBlock->setContent('Hello block world!'); $documentManager->persist($myBlock);

Note the sidebarBlock is the identifier we chose for the block. Together with the parent document of the block, this makes the block unique. The other properties are specific to Symfony\Cmf\Bundle\BlockBundle\Document\SimpleBlock. The simple block is now ready to be rendered, see Block rendering.
Always make sure you implement the interface Sonata\BlockBundle\Model\BlockInterface or an existing block document like Symfony\Cmf\Bundle\BlockBundle\Document\BaseBlock.

Block Context
The BlockContext contains all information and the block document needed to render the block. It aggregates and merges all settings from configuration, the block service, the block document and settings passed to the twig template helper. Therefore use the BlockContext to get or alter a setting if needed.

Block Service
If you look inside the SimpleBlock class, you will notice the method getType. This defines the name of the block service that processes the block when it is rendered. A block service contains: An execute method; Default settings; Dorm configuration; Cache configuration; Javascript and stylesheet assets to be loaded; A load method.

Take a look at the block services in Symfony\Cmf\Bundle\BlockBundle\Block to see some examples.


Always make sure you implement the interface Sonata\BlockBundle\Block\BlockServiceInterface or an existing block service like Sonata\BlockBundle\Block\BaseBlockService.

The Execute Method


This method contains controller logic:

PDF brought to you by generated on July 18, 2013

Chapter 13: The BlockBundle | 62

Listing 13-4

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// ... if ($block->getEnabled()) { $feed = false; if ($blockContext->getSetting('url', false)) { $feed = $this->feedReader->import($block); }


return $this->renderResponse($blockContext->getTemplate(), array( 'feed' => $feed, 'block' => $blockContext->getBlock(), 'settings' => $blockContext->getSettings(), ), $response); } // ...

If you have much logic to be used, you can move that to a specific service and inject it in the block service. Then use this specific service in the execute method.

Default Settings
The method setDefaultSettings specifies the default settings for a block. Settings can be altered on multiple places afterwards, it cascades like this: Default settings are stored in the block service; If you use a 3rd party bundle you might want to change them in the bundle configuration for your application see Configuration; Settings can be altered through template helpers (see example); And settings can also be altered in a block document, the advantage is that settings are stored in PHPCR and allows to implement a frontend or backend UI to change some or all settings. Example of how settings can be specified through a template helper:
Listing 13-5

1 {{ sonata_block_render({'name': 'rssBlock'}, { 2 'title': 'Symfony2 CMF news', 3 'url': 'http://cmf.symfony.com/news.rss' 4 }) }}

Form Configuration
The methods buildEditForm and buildCreateForm specify how to build the the forms for editing using a frontend or backend UI. The method validateBlock contains the validation configuration.

Cache Configuration
The method getCacheKeys contains cache keys to be used for caching the block.

Javascript and Stylesheets


The methods getJavascripts and getStylesheets can be used to define javascript and stylesheet assets. Use the twig helpers sonata_block_include_javascripts and sonata_block_include_stylesheets to render them:

PDF brought to you by generated on July 18, 2013

Chapter 13: The BlockBundle | 63

Listing 13-6

1 {{ sonata_block_include_javascripts() }} 2 {{ sonata_block_include_stylesheets() }}

This will output the javascripts and stylesheets for all blocks loaded in the service container of your application.

The Load Method


The method load can be used to load additional data. It is called each time a block is rendered before the execute method is called.

Block rendering
To render the example from the Block Document section, just add the following code to your Twig template:
Listing 13-7

1 {{ sonata_block_render({'name': '/cms/content/blocks/sidebarBlock'}) }}

In this example we specify an absolute path, however, if the block is a child of a content document, then you can simply specify the name of the block as follows:
Listing 13-8

1 {{ sonata_block_render({'name': 'sidebarBlock'}) }}

This will make the BlockBundle render the specified block on every page that has a child block document named sidebarBlock. Of course, the actual page needs to be rendered by the template that contains the snippet above. When a block is rendered the following things happen: The block document is loaded based on its name or absolute path; If caching is configured, the cache is checked and content is returned if found; The execute method of the corresponding block service is called. The execute method is the equivalent of a normal Symfony controller. It receives the block object (equivalent to a Request object) and a Response object. The purpose of the execute method to set the content of the response object - typically by rendering a Twig template. You can also embed blocks in content using the cmf_embed_blocks filter.

Block types
The block bundle comes with a couple of predefined blocks. You may write your own blocks, but often, the supplied implementations will be sufficient. This is just a quick overview, more details on each block type can be found in the Block Types section. There are five general purpose blocks: StringBlock: A block only containing a string that is rendered without any decoration. Useful for page fragments; SimpleBlock: A simple block with nothing but a title and a field of hypertext. This would usually be what an editor edits directly, for example contact information;
PDF brought to you by generated on July 18, 2013 Chapter 13: The BlockBundle | 64

ContainerBlock: A block that contains zero, one or many child blocks; ReferenceBlock: A block that references a block stored somewhere else in the content tree. For example you might want; to refer parts of the contact information from the homepage ActionBlock: A block that calls a Symfony2 action. The BlockBundle also provides a couple of blocks for specific tasks, integrating third party libraries. You should to read the Block Types section relevant to those blocks to figure out what third party libraries you need to load into your project. RssBlock: This block extends the ActionBlock, the block document saves the feed url and the controller action fetches the feed items. The default implementation uses the EkoFeedBundle6 to read the feed items. ImagineBlock: A block containing an image child, the imagine filter name and optional link url and title. SlideshowBlock: A special case of a container block suitable for building a slideshow of blocks. Note that this block doesn't provide any Javascript code to make the slideshow work in the frontend. You can use your favourite Javascript library to do the animation.

Examples
You can find example usages of this bundle in the Symfony CMF Sandbox7 (have a look at the BlockBundle). It also shows you how to make blocks editable using the CreateBundle.

6. https://github.com/eko/FeedBundle 7. https://github.com/symfony-cmf/cmf-sandbox

PDF brought to you by generated on July 18, 2013

Chapter 13: The BlockBundle | 65

Chapter 14

Block Types
The BlockBundle provides a couple of default block types for general use cases. It also has a couple of more specific blocks that integrate third party libraries. Those can be handy for some use cases and also serve as examples to build your own blocks.

StringBlock
This is a very simple block that just provides one string field called content and the default template renders the content as raw to allow HTML in the field. The template outputs no HTML tags around the string at all.

SimpleBlock
Just a text block with a title and a content. The default template renders both title and content as raw, meaning HTML is allowed in those fields. This block also exists in a MultilangSimpleBlock variant that can be translated. This block is useful to edit static text fragments and for example display it in several places using the ReferenceBlock.

ContainerBlock
A container can hold a list of arbitrary child blocks (even other ContainerBlocks) and just renders one child after the other. This block has the methods setChildren to overwrite the current children with a new list and addChild and removeChild to individually add resp. remove child blocks.

PDF brought to you by generated on July 18, 2013

Chapter 14: Block Types | 66

ReferenceBlock
This block has no content of its own but points to a target block. When rendered, this block renders the target node as if the target node was directly used in that place. This block simply has the method setReferencedBlock that accepts any block mapped by the persistence layer as argument. If you set this to something that is not a valid block, the problem is only detected when rendering the block.

ActionBlock
The action block allows to configure a controller action that will be called in a subrequest when rendering the block. Instead of directly calling the action from a template, your CMS users can define and parametrize their own actions, and decide where to put this block. This block is also a good base to implement specific actions if you need something more user friendly. See the RssBlock below for an example. As the ActionBlock does a subrequest, you may also need to control the parameters that are passed to the subrequest. The block service calls resolveRequestParams($request, $blockContext) to let the block decide what needs to be passed to the subrequest. The ActionBlock implementation lets you configure the fields with setRequestParams and persists them in the database. It does not matter whether the field is found in the request attributes or the request parameters, it is found in both by using $request->get(). The only request attribute propagated by default is the _locale.

RssBlock
The RssBlock extends the ActionBlock and allows you to read feed items and display them in a list. Create a document:
Listing 14-1

1 2 3 4 5 6 7 8 9 10 11 12

use Symfony\Cmf\Bundle\BlockBundle\Document\RssBlock;

// ...
$myRssBlock = new RssBlock(); $myRssBlock->setParentDocument($parentPage); $myRssBlock->setName('rssBlock'); $myRssBlock->setSetting('title', 'Symfony2 CMF news'); $myRssBlock->setSetting('url', 'http://cmf.symfony.com/news.rss'); $myRssBlock->setSetting('maxItems', 3); $documentManager->persist($myRssBlock);

All available settings are: url: the url of the rss feed (required) title: the title for the list (default: Insert the rss title) maxItems: the maximum amount of items to return to the template (default: 10) template: the template to render the feed items (default: CmfBlockBundle:Block:block_rss.html.twig) ItemClass: the class used for the item objects that are passed to the template (default: Symfony\Cmf\Bundle\BlockBundle\Model\FeedItem) The controller to get the feed items can also be changed:

PDF brought to you by generated on July 18, 2013

Chapter 14: Block Types | 67

Define a different class for the controller service in your configuration using the DI service parameter cmf_block.rss_controller_class or set the actionName of your RssBlock document
The Symfony CMF Sandbox1 contains an example of the RssBlock.

ImagineBlock
The imagine block uses the LiipImagineBundle2 to display images directly out of PHPCR. The block has a child of type nt:file and fields for the name of the imagine filter to use, an URL and an image caption. To use this block, you need to add liip/imagine-bundle to your composer.json and define the imagine filter you specify in the block. The default name is cmf_block. The filter must use the phpcr driver:
Listing 14-2

1 # app/config/config.yml 2 liip_imagine: 3 # ... 4 filter_sets: 5 cmf_block: 6 data_loader: phpcr 7 quality: 85 8 filters: 9 thumbnail: { size: [616, 419], mode: outbound } 10 # ...

Refer to the LiipImagineBundle documentation3 for further information. See the example below for how to create an ImagineBlock programmatically.

SlideshowBlock
The SlideshowBlock is just a special kind of ContainerBlock. It can contain any kind of blocks that will be rendered with a wrapper div to help a javascript slideshow library to slide them. The ImagineBlock is particularly suited if you want to do an image slideshow but the SlideshowBlock can handle any kind of blocks, also mixed types of blocks in the same slideshow.
This bundle does not attempt to provide a javascript library for animating the slideshow. Chose your preferred library that plays well with the rest of your site and hook it on the slideshows. (See also below).

Create your first Slideshow


Creating a slideshow consists of creating the container SlideshowBlock and adding blocks to it. Those blocks can be anything, but an image makes a lot of sense:
Listing 14-3

1. https://github.com/symfony-cmf/cmf-sandbox 2. https://github.com/liip/LiipImagineBundle 3. https://github.com/liip/LiipImagineBundle/tree/master/Resources/doc

PDF brought to you by generated on July 18, 2013

Chapter 14: Block Types | 68

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

use Symfony\Cmf\Bundle\BlockBundle\Document\SlideshowBlock; use Symfony\Cmf\Bundle\BlockBundle\Document\ImagineBlock; // the Image will be moved to Symfony\Cmf\Bundle\MediaBundle\Model\Image use Doctrine\ODM\PHPCR\Document\Image; use Doctrine\ODM\PHPCR\Document\File;

// create slideshow $mySlideshow = new SlideshowBlock(); $mySlideshow->setName('slideshow'); $mySlideshow->setParentDocument($parentPage); $mySlideshow->setTitle('My first Slideshow'); $documentManager->persist($mySlideshow); // add first slide to slideshow $mySlideshowItem = new ImagineBlock(); $mySlideshowItem->setName('first_item'); $mySlideshowItem->setLabel('label of first item'); $mySlideshowItem->setParentDocument($mySlideshow); $manager->persist($mySlideshowItem);
$file = new File(); $file->setFileContentFromFilesystem('path/to/my/image.jpg'); $image = new Image(); $image->setFile($file); $mySlideshowItem->setImage($image);

Render the slideshow


Rendering your slideshow is as easy as just rendering the according block in your template. If your contentDocument has a field slideshow that contains a SlideshowBlock object, you can simply render it with:
Listing 14-4

1 {{ sonata_block_render({ 2 'name': 'slideshow' 3 }) }}

Make the slideshow work in the frontend


Since the BlockBundle doesn't contain anything to make the slideshow work in the frontend, you need to do this yourself. Just use your favourite JS library to make the slideshow interactive. If special markup is needed for your slideshow code to work, just override BlockBundle:Block:block_slideshow.html.twig or the templates of the blocks you use as slideshow items and adapt them to your needs.

Use the Sonata admin class


The BlockBundle comes with an admin class for managing slideshow blocks. All you need to do to administrate slideshows in your project is to add the following line to your sonata admin configuration:
Listing 14-5

1 sonata_admin: 2 dashboard: 3 groups: 4 blocks: 5 label: Blocks

PDF brought to you by generated on July 18, 2013

Chapter 14: Block Types | 69

6 7

items: - cmf_block.slideshow_admin

However, you can also embed the slideshow administration directly into other admin classes using the sonata_type_admin form type. The admin service to use in that case is cmf_block.slideshow_admin. Please refer to the Sonata Admin documentation4 for further information.

4. http://sonata-project.org/bundles/admin/master/doc/reference/form_types.html

PDF brought to you by generated on July 18, 2013

Chapter 14: Block Types | 70

Chapter 15

Create your own Blocks


Follow these steps to create a block: create a block document; create a block service and declare it (optional); create a data object representing your block in the repository, see Block Document; render the block, see Block rendering;

Lets say you are working on a project where you have to integrate data received from several RSS feeds. Of course you could create an ActionBlock for each of these feeds, but wouldn't this be silly? In fact all those actions would look similar: Receive data from a feed, sanitize it and pass the data to a template. So instead you decide to create your own block, the RSSBlock.

Create a block document


The first thing you need is an document that contains the data. It is recommended to extend Symfony\Cmf\Bundle\BlockBundle\Document\BaseBlock contained in this bundle (however you are not forced to do so, as long as you implement Sonata\BlockBundle\Model\BlockInterface). In your document, you need to define the getType method which just returns acme_main.block.rss.
Listing 15-1

1 2 3 4 5 6 7 8 9 10 11 12 13 14

// src/Acme/MainBundle/Document/RssBlock.php namespace Acme\MainBundle\Document;


use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM; use Symfony\Cmf\Bundle\BlockBundle\Document\BaseBlock;

/** * Rss Block * * @PHPCRODM\Document(referenceable=true) */ class RssBlock extends BaseBlock { public function getType()

PDF brought to you by generated on July 18, 2013

Chapter 15: Create your own Blocks | 71

15 16 17 18 }

{ return 'acme_main.block.rss'; }

Create a Block Service


You could choose to use a an already existing block service because the configuration and logic already satisfy your needs. For our rss block we create a service that knows how to handle RSSBlocks: The method setDefaultSettings configures a template, title, url and the maximum amount of items:
Listing 15-2

1 2 3 4 5 6 7 8 9 10 11

// ... public function setDefaultSettings(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'template' => 'AcmeMainBundle::Block::block_rss.html.twig', 'url' => false, 'title' => 'Insert the rss title', 'maxItems' => 10, )); } // ...

The execute method passes the settings to an rss reader service and forwards The feed items to a template, see The Execute Method Make sure you implement the interface Sonata\BlockBundle\Block\BlockServiceInterface or an existing block service like Sonata\BlockBundle\Block\BaseBlockService. Define the service in a config file. It is important to tag your BlockService with sonata.block, otherwise it will not be known by the Bundle.
Listing 15-3

1 acme_main.rss_reader: 2 class: Acme\MainBundle\Feed\SimpleReader 3 4 sandbox_main.block.rss: 5 class: Acme\MainBundle\Block\RssBlockService 6 arguments: 7 - "acme_main.block.rss" 8 - "@templating" 9 - "@sonata.block.renderer" 10 - "@acme_main.rss_reader" 11 tags: 12 - {name: "sonata.block"}

PDF brought to you by generated on July 18, 2013

Chapter 15: Create your own Blocks | 72

Chapter 16

Cache
The BlockBundle integrates with the SonataCacheBundle1 to provide several caching solutions. Have a look at the available adapters in the SonataCacheBundle to see all options. The BlockBundle additionally provides its own adapters for: ESI2 SSI3 Asynchronous javascript Synchronous javascript
It is advised to store all settings in the block document when using cache. See also Block Rendering.

Dependencies
The cache functionality is optional and depends on the SonataCacheBundle4.

Installation
The installation is split between the SonataCacheBundle, the CmfBlockBundle and the SonataBlockBundle: 1. SonataCacheBundle - Follow the installation instructions from the SonataCacheBundle documentation5. 2. CmfBlockBundle - At the end of your routing file, add the following lines:
1. https://github.com/sonata-project/SonataCacheBundle 2. http://wikipedia.org/wiki/Edge_Side_Includes 3. http://wikipedia.org/wiki/Server_Side_Includes 4. https://github.com/sonata-project/SonataCacheBundle 5. http://sonata-project.org/bundles/cache/master/doc/index.html

PDF brought to you by generated on July 18, 2013

Chapter 16: Cache | 73

Listing 16-1

1 2 3 4 5 6 7

# app/config/routing.yml # ... # routes CmfBlockBundle cache adapters block_cache: resource: "@CmfBlockBundle/Resources/config/routing/cache.xml" prefix: /

3. SonataBlockBundle - Use the sonata_block key to configure the cache adapter for each block service:
Listing 16-2

1 # app/config/config.yml 2 sonata_block: 3 # ... 4 blocks: 5 acme_main.block.news: 6 # use the service id of the cache adapter 7 cache: cmf.block.cache.js_async 8 blocks_by_class: 9 # cache only the RssBlock and not all action blocks 10 Symfony\Cmf\Bundle\BlockBundle\Document\RssBlock: 11 cache: cmf.block.cache.js_async

Workflow
The following happens when a block is rendered using cache: A document is loaded based on the name If caching is configured, the cache is checked and content is returned if found Cache keys are computed using: The cache keys of the block service The extraCacheKeys passed from the template The cache adapter is asked for a cache element The ESI and SSI adapter add a specific tag and a url to retrieve the block content The Javascript adapter adds javascript and a url to retrieve the block content If the cache element is not expired and has data it is returned The template is rendered: For ESI and SSI the url is called to retrieve the block content For Javascript the browser calls a url and replaces a placeholder with the returned block content
The additional cache adapters of the BlockBundle always return that the cache is found, have a look at the has method of the adapters in the SonataCacheBundle to see how they respond.

PDF brought to you by generated on July 18, 2013

Chapter 16: Cache | 74

If cache is checked and the cache adapter returned that no cache was found, the workflow proceeds like this: Each block document also has a block service, the execute method of it is called to render the block and return a response If the response is cacheable the configured adapter creates a cache element, it contains The computed cache keys The ttl of the response The response And additional contextual keys

The template is rendered

Cache Keys
The block service has the responsibility to generate the cache keys, the method getCacheKeys returns these keys, see Block Service. The block services shipped with the BlockBunde use the getCacheKeys method of the Sonata\BlockBundle\Block\BaseBlockService, and return: block_id updated_at
If block settings need to be persisted between requests it is advised to store them in the block document. Alternatively they can be added to the cache keys. However be very cautious because, depending on the adapter, the cache keys can be send to the browser and are not secure.

Extra Cache Keys


The extra cache keys array is used to store metadata along the cache element. The metadata can be used to invalidate a set of cache elements.

Contextual Keys
The contextual cache array hold the object class and id used inside the template. This contextual cache array is then added to the extra cache key. This feature can be use like this $cacheManager->remove(array('objectId' => 'id')). Of course not all cache adapters support this feature, varnish and MongoDB do. The BlockBundle also has a cache invalidation listener that calls the flush method of a cache adapter automatically when a cached block document is updated or removed.

Block Rendering
The following parameters can be used in the sonata_block_render code in your Twig template when using cache: use_cache: use the configured cache for a block (default: true) extra_cache_keys: expects an array with extra cache keys (default: empty array)

PDF brought to you by generated on July 18, 2013

Chapter 16: Cache | 75

Listing 16-3

1 {{ sonata_block_render({ 'name': 'rssBlock' }, { 2 use_cache: true, 3 extra_cache_keys: { 'extra_key': 'my_block' } 4 }) }}

When using the Esi, Ssi or Js cache adapters the settings passed here are remembered:
Listing 16-4

1 {{ sonata_block_render({ 'name': 'rssBlock' }, { 2 'title': 'Symfony2 CMF news', 3 'url': 'http://cmf.symfony.com/news.rss', 4 'maxItems': 2 5 }) }}

The default BlockContextManager of the SonataBlockBundle automatically adds settings passed from the template to the extra_cache_keys with the key context. This allows the cache adapters to rebuild the BlockContext. See also the SonataBlockBundle Advanced usage6 documentation.
Secure the cache adapter url if needed as the settings from sonata_block_render are added to the url as parameters.

Because, as mentioned above, settings can be added to the url as parameters avoid exposing sensitive settings from sonata_block_render and try to store them in the block document.

Adapters
ESI
This extends the default VarnishCache adapter of the SonataCacheBundle.

Configuration
Listing 16-5

1 # app/config/config.yml 2 framework: 3 # ... 4 esi: { enabled: true } 5 # enable FragmentListener to automatically validate and secure fragments 6 fragments: { path: /_fragment } 7 # add varnish server ip-address(es) 8 trusted_proxies: [192.0.0.1, 10.0.0.0/8] 9 10 cmf_block: 11 # ... 12 caches: 13 varnish: 14 token: a unique security key # a random one is generated by default

6. http://sonata-project.org/bundles/block/master/doc/reference/advanced_usage.html#block-context-manager-context-cache

PDF brought to you by generated on July 18, 2013

Chapter 16: Cache | 76

15 16

servers: - varnishadm -T 127.0.0.1:2000 {{ COMMAND }} "{{ EXPRESSION }}"

SSI
This extends the default SsiCache adapter of the SonataCacheBundle.

Configuration
Listing 16-6

1 # app/config/config.yml 2 cmf_block: 3 # ... 4 caches: 5 ssi: 6 token: a unique security key # a random one is generated by default

Javascript
Renders the block using javascript, the page is loaded and not waiting for the block to be finished rendering or retrieving data. The block is then asynchronously or synchronously loaded and added to the page.

PDF brought to you by generated on July 18, 2013

Chapter 16: Cache | 77

Chapter 17

Relation to Sonata Block Bundle


The BlockBundle is based on the SonataBlockBundle1. It replaces components of the bundle where needed to be compatible with PHPCR.

Classdiagram
The following picture shows where we use our own components (blue): ../../../_images/classdiagram.jpg

1. https://github.com/sonata-project/SonataBlockBundle

PDF brought to you by generated on July 18, 2013

Chapter 17: Relation to Sonata Block Bundle | 78

Chapter 18

BlogBundle
This bundle aims to provide everything you need to create a full blog or blog-like system. It also includes in-built support for the Sonata Admin bundle to help you get up-and-running quickly. Current features: Host multiple blogs within a single website; Place blogs anywhere within your route hierarchy; Sonata Admin integration. Pending features: Full tag support; Frontend pagination (using knp-paginator); RSS/ATOM feed; Comments; User support (FOSUserBundle).

Notes on this document


The XML configuration examples may be formatted incorrectly.

Dependencies
CmfRoutingBundle is used to manage the routing; CmfRoutingAutoBundle is used to manage automatically generate routes; PHPCR-ODM is used to persist the bundles documents.

Configuration
Example:
Listing 18-1

PDF brought to you by generated on July 18, 2013

Chapter 18: BlogBundle | 79

1 # app/config.yml 2 cmf_blog: 3 use_sonata_admin: auto 4 blog_basepath: /cms/blog 5 class: 6 blog_admin: Symfony\Cmf\Bundle\BlogBundle\Admin\BlogAdmin # Optional 7 post_admin: Symfony\Cmf\Bundle\BlogBundle\Admin\PostAdmin # Optional 8 blog: Symfony\Cmf\Bundle\BlogBundle\Document\Blog # Optional 9 post: Symfony\Cmf\Bundle\BlogBundle\Document\Post # Optional

Explanation: use_sonata_admin - Specify whether to attempt to integrate with sonata admin; blog_basepath - required Specify the path where the blog content should be placed when using sonata admin; class - Allows you to specify custom classes for sonata admin and documents; * blog_admin: FQN of the sonata admin class to use for managing Blog's; * post_admin: FQN of the sonata admin class to use for managing Post's; * blog: FQN of the document class that sonata admin will use for Blog's; * post: FQN of the document class that sonata admin will use for Post's.
If you change the default documents it is necessary to update the auto routing configuration, as the auto routing system will not recognize your new classes and consequently will not generate any routes.

Auto Routing
The blog bundle uses the CmfRoutingAuto bundle to generate a route for each content. You will need an auto routing configuration for this to work. You can include the default in the main configuration file as follows:
Listing 18-2

# app/config/config.yml imports: # ... - { resource: @CmfBlogBundle/Resources/config/routing/autoroute_default.yml } # ...

The default configuration will produce URLs like the following:


Listing 18-3

1 http://www.example.com/blogs/dtls-blog/2013-04-14/this-is-my-post

Refer to the RoutingAutoBundle documentation for more information.

Content Routing
To enable the routing system to automatically forward requests to the blog controller when a Blog or Post content is associated with a route, add the following under the controllers_by_class section of cmf_routing_extra in the main configuration file:
Listing 18-4

1 # app/config/config.yml 2 cmf_routing_extra: 3 # ... 4 dynamic: 5 # ...

PDF brought to you by generated on July 18, 2013

Chapter 18: BlogBundle | 80

6 7 8 9

controllers_by_class: # ... Symfony\Cmf\Bundle\BlogBundle\Document\Blog: cmf_blog.blog_controller:listAction Symfony\Cmf\Bundle\BlogBundle\Document\Post: cmf_blog.blog_controller:viewPostAction

Sonata Admin
The BlogBundle has admin services defined for Sonata Admin, to make the blog system visible on your dashboard, add the following to the sonata_admin section:
Listing 18-5

1 # app/config/config.yml 2 sonata_admin: 3 # ... 4 dashboard: 5 groups: 6 # ... 7 blog: 8 label: blog 9 items: 10 - cmf_blog.admin 11 - cmf_post.admin

Tree Browser Bundle


If you use the Symfony CMF Tree Browser bundle you can expose the blog routes to enable blog edition from the tree browser. Expose the routes in the fos_js_routing section of the configuration file:
Listing 18-6

1 # app/config/config.yml 2 fos_js_routing: 3 routes_to_expose: 4 # ... 5 - admin_bundle_blog_blog_create 6 - admin_bundle_blog_blog_delete 7 - admin_bundle_blog_blog_edit

Integration
Templating
The default templates are marked up for Twitter Bootstrap1. But it is easy to completely customize the templates by overriding them. The one template you will have to override is the default layout, you will need to change it and make it extend your applications layout. The easiest way to do this is to create the following file:
Listing 18-7

1 {# app/Resources/CmfBlogBundle/views/default_layout.html.twig #} 2 {% extends "MyApplicationBundle::my_layout.html.twig" %} 3

1. http://twitter.github.com/bootstrap/

PDF brought to you by generated on July 18, 2013

Chapter 18: BlogBundle | 81

4 {% block content %} 5 {% endblock %}

The blog will now use MyApplicationBundle::my_layout.html.twig CmfBlogBundle::default_layout.html.twig. See `Overriding Bundle Templates`_ in the Symfony documentation for more information.

instead

of

PDF brought to you by generated on July 18, 2013

Chapter 18: BlogBundle | 82

Chapter 19

ContentBundle
This bundle provides a document for static content and the controller to render it. For an introduction see the Content article in the "Getting started" section.

Configuration
The configuration key for this bundle is cmf_content:
Listing 19-1

1 # app/config/config.yml 2 cmf_content: 3 admin_class: 4 document_class: 5 default_template: 6 content_basepath: 7 static_basepath: 8 use_sonata_admin: 9 multilang: 10 admin_class: 11 document_class: 12 use_sonata_admin: 13 locales: locale

~ ~ ~ /cms/content /cms/content/static auto # the whole multilang section is optional ~ ~ auto [] # if you use multilang, you have to define at least one

PDF brought to you by generated on July 18, 2013

Chapter 19: ContentBundle | 83

Chapter 20

CoreBundle
This is the CoreBundle1 for the Symfony2 content management framework. This bundle provides common functionality, helpers and utilities for the other CMF bundles. One of the provided features is an interface and implementation of a publish workflow checker with an accompanying interface that models can implement that want to support this checker. Furthermore it provides a twig helper exposing several useful functions for twig templates to interact with PHPCR-ODM documents.

Configuration
Listing 20-1

1 # app/config/config.yml 2 cmf_core: 3 document_manager_name: ~ # used for the twig functions to fetch documents 4 publish_workflow: 5 enabled: true 6 view_non_published_role: ROLE_CAN_VIEW_NON_PUBLISHED 7 request_listener: true

The publish workflow is enabled by default. If you do not want to use it, you can set cmf_core.publish_workflow.enabled: false to gain some performance.

Publish Workflow
The publish workflow system allows to control what content is available on the site. This is similar to the Symfony2 Security component2. But contrary to the security context, the publish check can be executed even when no firewall is in place and the security context thus has no token (see Symfony2 Authorization3).
1. https://github.com/symfony-cmf/CoreBundle#readme 2. http://www.symfony.com/doc/current/components/security/index.html 3. http://www.symfony.com/doc/current/components/security/authorization.html

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 84

The publish workflow is also tied into the security workflow: The core bundle registers a security voter that forwards security checks to the publish workflow. This means that if you always have a firewall, you can just use the normal security context and the twig function is_granted to check for publication. A good introduction to the Symfony core security is the Security Chapter4 in the Symfony2 book.

Check if Content is Published


The Bundle provides the cmf_core.publish_workflow.checker service which implements the SecurityContextInterface5 of the Symfony security component. The method to check publication is, like with the security context, isGranted()6. This method is used as when doing ACL checks7: The first argument is the desired action, the second the content object you want to do the action on. In 1.0, the only actions supported by the default voters are VIEW and VIEW_ANONYMOUS. Having the right to view means that the current user is allowed to see this content either because it is published or because of his specific permissions. In some contexts, your application might not want to show unpublished content even to a privileged user so as not to confuse him. For this, the "view anonymous" permission is used. The workflow checker is configured with a role that is allowed to bypass publication checks so that it can see unpublished content. This role should be given to editors. The default name of the role is ROLE_CAN_VIEW_NON_PUBLISHED.
Listing 20-2

1 # app/config/security.yml 2 security: 3 role_hierarchy: 4 ROLE_EDITOR:

ROLE_CAN_VIEW_NON_PUBLISHED

Once a user with ROLE_EDITOR is logged in - meaning there is a firewall in place for the path in question - he will have the permission to view unpublished content as well:
Listing 20-3

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker;

// check if current user is allowed to see this document $publishWorkflowChecker = $container->get('cmf_core.publish_workflow.checker'); if ($publishWorkflowChecker->isGranted( PublishWorkflowChecker::VIEW_ATTRIBUTE, $document) ) { // ... } // check if the document is published. even if the current role would allow // to see the document, this will still return false if the documet is not // published if ($publishWorkflowChecker->isGranted( PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $document )) { // ... }

To check publication in a template, use the twig function cmf_is_published:


Listing 20-4

4. http://www.symfony.com/doc/current/book/security.html 5. http://api.symfony.com/master/Symfony/Component/Security/Core/SecurityContextInterface.html 6. http://api.symfony.com/master/Symfony/Component/Security/Core/SecurityContextInterface.html#isGranted() 7. http://www.symfony.com/doc/current/cookbook/security/acl.html

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 85

1 2 3 4 5 6 7 8 9 10 11 12 13

{# check if document is published, regardless of current users role #} {% if cmf_is_published(page) %} {# ... output the document #} {% endif %} {# check if current logged in user is allowed to view the document either because it is published or because the current user may view unpublished documents. #} {% if is_granted('VIEW', page) %} {# ... output the document #} {% endif %}

Code that loads content should do the publish checks. Note that the twig functions already check for publication. Thanks to a request listener, routes and the main content provided by the DynamicRouter are checked automatically as well. It is possible to set the security token explicitly on the workflow checker. But by default, the checker will acquire the token from the default security context, and if there is none (typically when there is no firewall in place for that URL), an AnonymousToken8 is created on the fly. If you check for VIEW and not VIEW_ANONYMOUS, the first check is whether the security context knows the current user and if that user is granted the bypass role. If so, access is granted, otherwise the decision is delegated to a AccessDecisionManager9 which calls all voters with the requested attributes, the object and the token. The decision manager is configured for an unanimous vote with "allow if all abstain". This means a single voter saying ACCESS_DENIED is enough for the content to be considered not published. If all voters abstain (for example when the content in question does not implement any workflow features) the content is still considered published.

Publish Voters
A voter has to implement the VoterInterface10. It will get passed a content object and has to decide whether it is published according to its rules. The CoreBundle provides a couple of generic voters that check the content for having an interface exposing the methods they need. If the content implements the interface, they check the parameter and return ACCESS_GRANTED or ACCESS_DENIED, otherwise they return ACCESS_ABSTAIN. As voting is unanimous, each voter returns ACCESS_GRANTED if its criteria is met, but if a single voter returns ACCESS_DENIED, the content is considered not published. You can also implement your own voters for additional publication behaviour.

PublishableVoter
This voter checks on the PublishableInterface which simply has a method to return a boolean value. isPublishable: If the object should be considered for publication or not.

TimePeriodVoter
This voter checks on the PublishTimePeriodInterface which defines a start and end date. A date may be null to indicate "always started" resp. "never ending".
8. http://api.symfony.com/master/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.html 9. http://api.symfony.com/master/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.html 10. http://api.symfony.com/master/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.html

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 86

getPublishStartDate: If non-null, the date from which the document should start being published; getPublishEndDate: If non-null, the date from which the document should stop being published.

Custom Voters
To build voters with custom logic, you need to implement VoterInterface11 and define a service with the tag cmf_published_voter. This is similar to the security.voter tag, but adds your voter to the publish workflow. As with the security voters, you can specify a priority, though it is of limited use as the access decision must be unanimous. If you have more expensive checks, you can lower the priority of those voters.
Listing 20-5

services: acme.security.publishable_voter: class: %my_namespace.security.publishable_voter.class% tags: - { name: cmf_published_voter, priority: 30 }

As the workflow checker will create an AnonymousToken12 on the fly if the security context has none, voters must be able to handle this situation when accessing the user. Also when accessing the security context, they first must check if it has a token and otherwise not call it to avoid triggering an exception. If a voter only gives access if there is a current user fulfills some requirement, it simply has to return ACCESS_DENIED if there is no current user.

Publication Request Listener


The DynamicRouter places the route object and the main content - if the route has a main content into the request attributes. Unless you disable the cmf_core.publish_workflow.request_listener, this listener will listen on all requests and check publication of both the route object and the main content object. This means that custom templates for templates_by_class and the controllers_by_class need not check for publication explicitly as its already done. controllers of

Editing publication information: Publish Workflow Sonata Admin Extension


There is a write interface for each publish workflow too, defining setter methods. The core bundle provides extensions for SonataAdminBundle to easily add editing of the publish workflow fields to all or selected admins. Instead of implementing PublishableInterface resp. PublishTimePeriodInterface you models instead need to implement the PublishableWriteInterface and / or PublishTimePeriodWriteInterface. To enable the extensions in your admin classes, simply define the extension configuration in the sonata_admin section of your project configuration:
Listing 20-6

1 # app/config/config.yml 2 sonata_admin: 3 # ... 4 extensions: 5 symfony_cmf_core.publish_workflow.admin_extension.publishable: 6 implements: 7 - Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface

11. http://api.symfony.com/master/Symfony/Component/Security/Core/Authentication/Voter/VoterInterface.html 12. http://api.symfony.com/master/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.html

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 87

8 9 10

symfony_cmf_core.publish_workflow.admin_extension.time_period: implements: Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodWriteInterface

See the Sonata Admin extension documentation13 for more information.

Dependency Injection Tags


cmf_request_aware
If you have services that need the request (e.g. for the publishing workflow or current menu item voters), you can tag them with cmf_request_aware to have a kernel listener inject the request. Any class used in such a tagged service must have the setRequest method or you will get a fatal error:
Listing 20-7

1 2 3 4 5 6 7 8 9 10 11

use Symfony\Component\HttpFoundation\Request; class MyClass { private $request; public function setRequest(Request $request) { $this->request = $request; } }

You should only use this tag on services that will be needed on every request. If you use this tag excessively you will run into performance issues. For seldom used services, you can inject the container in the service definition and call $this->container->get('request') in your code when you actually need the request.

For Symfony 2.3, this tag is automatically translated to a synchronized service14 but as Symfony 2.2 does not have that feature, you can use this tag for bundles that you want to be able to use with Symfony 2.2. In custom applications that run with Symfony 2.3, there is no need for this tag, just use the synchronized service feature.

cmf_published_voter
Used to activate custom voters for the publish workflow . Tagging a service with cmf_published_voter integrates it into the access decision of the publish workflow. This tag has the attribute priority. The lower the priority number, the earlier the voter gets to vote.

13. http://sonata-project.org/bundles/admin/master/doc/reference/extensions.html 14. http://symfony.com/doc/current/cookbook/service_container/scopes.html#using-a-synchronized-service

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 88

Templating
Twig
<<<<<<< HEAD The core bundle contains a Twig extension that provides a set of useful functions for your templates. The functions respect the publish workflow if it is cmf_find: returns the document for the provided path cmf_find_many: returns an array of documents for the provided paths cmf_is_published: checks if the provided document is published, see bundle-corepublish_workflow-twig_function. cmf_prev: returns the previous document by examining the child nodes of the provided parent cmf_prev_linkable: returns the previous linkable document by examining the child nodes of the provided parent cmf_next: returns the next document by examining the child nodes of the provided parent cmf_next_linkable: returns the next linkable document by examining the child nodes of the provided parent cmf_child: returns a child documents of the provided parent document and child node cmf_children: returns an array of all the children documents of the provided parent cmf_linkable_children: returns an array of all the linkable children documents of the provided parent cmf_descendants: returns an array of all descendants paths of the provided parent cmf_document_locales: gets the locales of the provided document cmf_nodename: returns the node name of the provided document cmf_parent_path: returns the parent path of the provided document cmf_path: returns the path of the provided document
Listing 20-8

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

{% set page = cmf_find('/some/path') %} {% if cmf_is_published(page) %} {% set prev = cmf_prev(page) %} {% if prev %} <a href="{{ path(prev) }}">prev</a> {% endif %} {% set next = cmf_next(page) %} {% if next %} <span style="float: right; padding-right: 40px;"><a href="{{ path(next) }}">next</a></span> {% endif %} {% for news in cmf_children(page)|reverse %} <li><a href="{{ path(news) }}">{{ news.title }}</a> ({{ news.publishStartDate | date('Y-m-d') }})</li> {% endfor %} {% if 'de' in cmf_document_locales(page) %} <a href="{{ path( app.request.attributes.get('_route'), app.request.attributes.get('_route_params')|merge(app.request.query.all)|merge({ '_locale': 'de' }) ) }}">DE</a> {% endif %} {% if 'fr' in cmf_document_locales(page) %}

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 89

30 <a href="{{ path( 31 app.request.attributes.get('_route'), 32 33 app.request.attributes.get('_route_params')|merge(app.request.query.all)|merge({ 34 '_locale': 'fr' }) ) }}">FR</a> {% endif %} {% endif %}

PHP
The bundle also provides a templating helper to use in PHP templates, it contains the following methods:
Listing 20-9

find: returns the document for the provided path findMany: returns an array of documents for the provided paths isPublished: checks if the provided document is published getPrev: returns the previous document by examining the child nodes of the provided parent getPrevLinkable: returns the previous linkable document by examining the child nodes of the provided parent getNext: returns the next document by examining the child nodes of the provided parent getNextLinkable: returns the next linkable document by examining the child nodes of the provided parent getChild: returns a child documents of the provided parent document and child node getChildren: returns an array of all the children documents of the provided parent getLinkableChildren: returns an array of all the linkable children documents of the provided parent getDescendants: returns an array of all descendants paths of the provided parent getLocalesFor: gets the locales of the provided document getNodeName: returns the node name of the provided document getParentPath: returns the parent path of the provided document getPath: returns the path of the provided document

1 <?php $page = $view['cmf']->find('/some/path') ?> 2 3 <?php if $view['cmf']->isPublished($page) : ?> 4 <?php $prev = $view['cmf']->getPrev($page) ?> 5 <?php if ($prev) : ?> 6 <a href="<?php echo $view['router']->generate($prev) ?>">prev</a> 7 <?php endif ?> 8 9 <?php $next = $view['cmf']->getNext($page) ?> 10 <?php if ($next) : ?> 11 <span style="float: right; padding-right: 40px;"> 12 <a href="<?php echo $view['router']->generate($next) ?>">next</a> 13 </span> 14 <?php endif ?> 15 16 <?php foreach (array_reverse($view['cmf']->getChildren($page)) as $news) : ?> 17 <li> 18 <a href="<?php echo $view['router']->generate($news) ?>"><?php echo 19 $news->getTitle() ?></a> 20 (<?php echo date('Y-m-d', $news->getPublishStartDate()) ?>) 21 </li> 22 <?php endforeach ?>

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 90

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

<?php if (in_array('de', $view['cmf']->getLocalesFor($page))) : ?> <a href="<?php $view['router']->generate $app->getRequest()->attributes->get('_route'), array_merge( $app->getRequest()->attributes->get('_route_params'), array_merge( $app->getRequest()->query->all(), array('_locale' => 'de') ) ) ?>">DE</a> <?php endif ?> <?php if (in_array('fr', $view['cmf']->getLocalesFor($page))) : ?> <a href="<?php $view['router']->generate $app->getRequest()->attributes->get('_route'), array_merge( $app->getRequest()->attributes->get('_route_params'), array_merge( $app->getRequest()->query->all(), array('_locale' => 'fr') ) ) ?>">FR</a> <?php endif ?> <?php endif ?>

PDF brought to you by generated on July 18, 2013

Chapter 20: CoreBundle | 91

Chapter 21

CreateBundle
The CreateBundle1 integrates create.js and the createphp helper library into Symfony2. Create.js is a comprehensive web editing interface for Content Management Systems. It is designed to provide a modern, fully browser-based HTML5 environment for managing content. Create can be adapted to work on almost any content management backend. The default editor is the Hallo Editor, but you can also use CKEditor. See http://createjs.org/2 for more information. Createphp is a PHP library to help with RDFa annotating your documents/entities. See https://github.com/flack/createphp3 for documentation on how it works.

Dependencies
This bundle includes create.js (which bundles all its dependencies like jquery, vie, hallo, backbone etc) as a git submodule. Do not forget to add the composer script handler to your composer.json as described below. PHP dependencies are managed through composer. We use createphp as well as AsseticBundle, FOSRestBundle and by inference also JmsSerializerBundle. Make sure you instantiate all those bundles in your kernel and properly configure assetic.

Installation
This bundle is best included using Composer. Edit your project composer file to add a new require for symfony-cmf/create-bundle. Then create a scripts section or add to the existing one:
Listing 21-1

1 { 2

"scripts": {

1. https://github.com/symfony-cmf/CreateBundle 2. http://createjs.org/ 3. https://github.com/flack/createphp

PDF brought to you by generated on July 18, 2013

Chapter 21: CreateBundle | 92

3 4 5 6 7 8 9 10 11 12 }

"post-install-cmd": [ "Symfony\\Cmf\\Bundle\\CreateBundle\\Composer\\ScriptHandler::downloadCreate", ... ], "post-update-cmd": [ "Symfony\\Cmf\\Bundle\\CreateBundle\\Composer\\ScriptHandler::downloadCreate", ... ] }

It is possible to specify another directory, repository or commit id in the extra parameters of composer.json file (here are the default values):
Listing 21-2

1 { 2 "extra": { 3 "create-directory": "vendor/symfony-cmf/create-bundle/Symfony/Cmf/Bundle/ 4 CreateBundle/Resources/public/vendor/create", 5 "create-repository": "https://github.com/bergie/create.git", 6 "create-commit": "271e0114a039ab256ffcceacdf7f361803995e05" 7 } }

Add this bundle (and its dependencies, if they are not already there) to your application's kernel:
Listing 21-3

1 2 3 4 5 6 7 8 9 10 11 12

// application/ApplicationKernel.php public function registerBundles() { return array( // ... new Symfony\Bundle\AsseticBundle\AsseticBundle(), new JMS\SerializerBundle\JMSSerializerBundle($this), new FOS\RestBundle\FOSRestBundle(), new Symfony\Cmf\Bundle\CreateBundle\CmfCreateBundle(), // ... ); }

You also need to configure FOSRestBundle to handle json:


Listing 21-4

1 fos_rest: 2 view: 3 formats: 4 json: true

Using CKEditor Instead


If you want to use CKEditor, edit the composer.json file to call downloadCreateAndCkeditor instead of downloadCreate:
Listing 21-5

1 { 2 "scripts": { 3 "post-install-cmd": [ 4 5 "Symfony\\Cmf\\Bundle\\CreateBundle\\Composer\\ScriptHandler::downloadCreateAndCkeditor",

PDF brought to you by generated on July 18, 2013

Chapter 21: CreateBundle | 93

6 ... 7 ], 8 "post-update-cmd": [ 9 10 "Symfony\\Cmf\\Bundle\\CreateBundle\\Composer\\ScriptHandler::downloadCreateAndCkeditor", 11 ... 12 ] } }

and re-run composer:


Listing 21-6

1 $ php composer.phar update nothing

In your application config file, define the editor base path:


Listing 21-7

1 cmf_create: 2 editor_base_path: /bundles/cmfcreate/vendor/ckeditor/

In your template, load the javascript files using:


Listing 21-8

1 {% render controller( 2 "cmf_create.jsloader.controller:includeJSFilesAction", 3 {"editor": "ckeditor"} 4 ) %}

As for create.js, you can override the source of CKEditor to a different target directory, source repository or commit id in the extra parameters of the composer.json file (here are the default values):
Listing 21-9

1 { 2 "extra": { 3 "ckeditor-directory": "vendor/symfony-cmf/create-bundle/Symfony/Cmf/Bundle/ 4 CreateBundle/Resources/public/vendor/ckeditor", 5 "ckeditor-repository": "https://github.com/ckeditor/ckeditor-releases.git", 6 "ckeditor-commit": "bba29309f93a1ace1e2e3a3bd086025975abbad0" 7 } }

Concept
Createphp uses RDFa metadata about your domain classes, much like doctrine knows the metadata how an object is stored in the database. The metadata is modelled by the type class and can come from any source. Createphp provides metadata drivers that read XML, php arrays and one that just introspects objects and creates non-semantical metadata that will be enough for create.js to edit. The RdfMapper is used to translate between your storage layer and createphp. It is passed the domain object and the relevant metadata object. With the metadata and the twig helper, the content is rendered with RDFa annotations. create.js is loaded and enables editing on the entities. Save operations happen in ajax calls to the backend. The REST controller handles those ajax calls, and if you want to be able to upload images, an image controller saves uploaded images and tells the image location.

PDF brought to you by generated on July 18, 2013

Chapter 21: CreateBundle | 94

Configuration
Listing 21-10

1 # app/config/config.yml 2 cmf_create: 3 # metadata loading 4 5 # directory list to look for metadata 6 rdf_config_dirs: 7 - "%kernel.root_dir%/Resources/rdf-mappings" 8 # look for mappings in <Bundle>/Resources/rdf-mappings 9 # auto_mapping: true 10 11 # use a different class for the REST handler 12 # rest_controller_class: FQN\Classname 13 14 # image handling 15 image: 16 model_class: ~ 17 controller_class: ~ 18 19 # access check role for js inclusion, default REST and image controllers 20 # role: IS_AUTHENTICATED_ANONYMOUSLY 21 22 # enable the doctrine PHPCR-ODM mapper 23 phpcr_odm: true 24 25 # mapping from rdf type name => class name used when adding items to collections 26 map: 27 rdfname: FQN\Classname 28 29 # stanbol url for semantic enhancement, otherwise defaults to the demo install 30 # stanbol_url: http://dev.iks-project.eu:8081 31 32 # fix the Hallo editor toolbar on top of the page 33 # fixed_toolbar: true 34 35 # RDFa types used for elements to be edited in plain text 36 # plain_text_types: ['dcterms:title'] 37 38 # RDFa types for which to create the corresponding routes after 39 # content of these types has been added with Create.js. This is 40 # not necessary with the SimpleCmsBundle, as the content and the 41 # routes are in the same repository tree. 42 # create_routes_types: ['http://schema.org/NewsArticle']

The provided javascript file configures create.js and the hallo editor. It enables some plugins like the tag editor to edit skos:related collections of attributes. We hope to add some configuration options to tweak the configuration of create.js but you can also use the file as a template and do your own if you need larger customizations.

Metadata
Createphp needs metadata information for each class of your domain model. By default, the create bundle uses the XML metadata driver and looks for metadata in the enabled bundles at <Bundle>/ Resources/rdf-mappings. If you use a bundle that has no RDFa mapping, you can specify a list of rdf_config_dirs that will additionally be checked for metadata. See the documentation of createphp4 for the format of the XML metadata format.
PDF brought to you by generated on July 18, 2013 Chapter 21: CreateBundle | 95

Access Control
If you use the default REST controller, everybody can edit content once you enabled the create bundle. To restrict access, specify a role other than the default IS_AUTHENTICATED_ANONYMOUSLY to the bundle. If you specify a different role, create.js will only be loaded if the user has that role and the REST handler (and image handler if enabled) will check the role. If you need more fine grained access control, look into the mapper isEditable method. You can extend the mapper you use and overwrite isEditable to answer whether the passed domain object is editable.

Image Handling
Enable the default simplistic image handler with the image > model_class | controller_class settings. This image handler just throws images into the PHPCR-ODM repository and also serves them in requests. If you need different image handling, you can either overwrite image.model_class and/or image.controller_class, or implement a custom ImageController and override the cmf_create.image.controller service with it.

Mapping Requests to Objects


For now, the bundle only provides a service to map to doctrine PHPCR-ODM. Enable it by setting phpcr_odm to true. If you need something else, you need to provide a service cmf_create.object_mapper. (If you need a wrapper for doctrine ORM, look at the mappers in the createphp library and do a pull request on that library, and another one to expose the ORM mapper as service in the create bundle). Also note that createphp would support different mappers for different RDFa types. If you need that, dig into the createphp and create bundle and do a pull request to enable this feature. To be able to create new objects, you need to provide a map between the RDFa types and the class names.

Routing
Finally add the relevant routing to your configuration
Listing 21-11

1 create: 2 resource: "@CmfCreateBundle/Resources/config/routing/rest.xml" 3 create_image: 4 resource: "@CmfCreateBundle/Resources/config/routing/image.xml"

Usage
Adjust your template to load the editor js files if the current session is allowed to edit content. If you are using Symfony 2.2 or higher:
Listing 21-12

1 {% render controller("cmf_create.jsloader.controller:includeJSFilesAction", {'_locale': app.request.locale}) %}

For versions prior to 2.2, this will do:


Listing 21-13

4. https://github.com/flack/createphp

PDF brought to you by generated on July 18, 2013

Chapter 21: CreateBundle | 96

1 {% render "cmf_create.jsloader.controller:includeJSFilesAction" with {'_locale': app.request.locale} %}

Plus make sure that assetic is rewriting paths in your css files, then include the base css files (and customize with your css as needed) with
Listing 21-14

1 {% include "CmfCreateBundle::includecssfiles.html.twig" %}

The other thing you have to do is provide RDFa mappings for your model classes and adjust your templates to render with createphp so that create.js knows what content is editable. Create XML metadata mappings in <Bundle>/Resources/rdf-mappings or a path you configured in rdf_config_dirs named after the full classname of your model classes with \\ replaced by a dot (.), i.e. Symfony.Cmf.Bundle.SimpleCmsBundle.Document.MultilangPage.xml. For an example mapping see the files in the cmf-sandbox. Reference documentation is in the createphp library repository5. To render your model, use the createphp twig tag:
Listing 21-15

1 {% createphp page as="rdf" %} 2 {{ rdf|raw }} 3 {% endcreatephp %}

Or if you need more control over the generated HTML:


Listing 21-16

1 2 3 4 5 6

{% createphp page as="rdf" %} <div {{ createphp_attributes(rdf) }}> <h1 class="my-title" {{ createphp_attributes( rdf.title ) }}>{{ createphp_content( rdf.title ) }}</h1> <div {{ createphp_attributes( rdf.body ) }}>{{ createphp_content( rdf.body ) }}</div> </div> {% endcreatephp %}

Alternative Editors
You can write your own templates to load a javascript editor. They have to follow the naming pattern CmfCreateBundle::includejsfiles-%editor%.html.twig to be loaded. In the includeJSFilesAction, you specify the editor parameter. (Do not forget to add the controller call around the controller name inside render for Symfony 2.2, as in the example above.)
Listing 21-17

1 {% render "cmf_create.jsloader.controller:includeJSFilesAction" with {'editor': 'aloha', '_locale': app.request.locale } %}

Create.js has built in support for Aloha and ckeditor, as well as the default hallo editor. Those should be supported by the CreateBundle as well. See these github issue for ckeditor6 and alhoa7 integration. If you wrote the necessary code for one of those editors, or another editor that could be useful for others, please send a pull request.

5. https://github.com/flack/createphp 6. https://github.com/symfony-cmf/CreateBundle/issues/33 7. https://github.com/symfony-cmf/CreateBundle/issues/32

PDF brought to you by generated on July 18, 2013

Chapter 21: CreateBundle | 97

Developing the Hallo Wysiwyg Editor


You can develop the hallo editor inside the Create bundle. By default, a minimized version of hallo that is bundled with create is used. To develop the actual code, you will need to checkout the full hallo repository first. You can do this by running the following command from the command line:
Listing 21-18

1 $ php app/console cmf:create:init-hallo-devel

There is a special template to load the coffee script files. To load this, just use the hallo-coffee editor with the includeJSFilesAction. (Do not forget to add the controller call around the controller name inside render for Symfony 2.2, as in the example above.)
Listing 21-19

1 {% render "cmf_create.jsloader.controller:includeJSFilesAction" with {'editor': 'hallo-coffee', '_locale': app.request.locale } %}

The hallo-coffee template uses assetic to load the coffee script files from Resources/public/vendor/ hallo/src, rather than the precompiled javascript from Resources/public/vendor/create/deps/ hallo-min.js. This also means that you need to add a mapping for coffeescript in your assetic configuration and you need the coffee compiler set up correctly8.
Listing 21-20

assetic: filters: cssrewrite: ~ coffee: bin: %coffee.bin% node: %coffee.node% apply_to: %coffee.extension%

In the cmf sandbox we did a little hack to not alwas trigger coffee script compiling. In config.yml we make the coffee extension configurable. Now if the parameters.yml sets coffee.extension to \.coffee the coffeescript is compiled and the coffee compiler needs to be installed. If you set it to anything else like \.nocoffee then you do not need the coffee compiler installed. The default values for the three parameters are
Listing 21-21

1 2 3 4 5 6

# app/config/parameters.yml # ... coffee.bin: /usr/local/bin/coffee coffee.node: /usr/local/bin/node coffee.extension: \.coffee

8. http://coffeescript.org/#installation

PDF brought to you by generated on July 18, 2013

Chapter 21: CreateBundle | 98

Chapter 22

DoctrinePHPCRBundle
The DoctrinePHPCRBundle1 provides integration with the PHP content repository and optionally with Doctrine PHPCR-ODM to provide the ODM document manager in symfony. Out of the box, this bundle supports the following PHPCR implementations: Jackalope2 (both jackrabbit and doctrine-dbal transports) Midgard23
This reference only explains the Symfony2 integration of PHPCR and PHPCR-ODM. To learn how to use PHPCR refer to the PHPCR website4 and for Doctrine PHPCR-ODM to the PHPCR-ODM documentation5.

This bundle is based on the AbstractDoctrineBundle and thus is similar to the configuration of the Doctrine ORM and MongoDB bundles.

Setup and Requirements


See Installing and Configuring Doctrine PHPCR-ODM

Configuration
If you want to only use plain PHPCR without the PHPCR-ODM, you can simply not configure the odm section to avoid loading the services at all. Note that most CMF bundles by default use PHPCR-ODM documents and thus need ODM enabled.

1. https://github.com/doctrine/DoctrinePHPCRBundle 2. http://jackalope.github.com/ 3. http://midgard-project.org/phpcr/ 4. http://phpcr.github.com/ 5. http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 99

PHPCR Session Configuration


The session needs a PHPCR implementation specified in the backend section by the type field, along with configuration options to bootstrap the implementation. Currently we support jackrabbit, doctrinedbal and midgard2. Regardless of the backend, every PHPCR session needs a workspace, username and password.
Every PHPCR implementation should provide the workspace called default, but you can choose a different one. There is the doctrine:phpcr:workspace:create command to initialize a new workspace. See also Services.

The username and password you specify here are what is used on the PHPCR layer in the PHPCR\SimpleCredentials. They will usually be different from the username and password used by Midgard2 or Doctrine DBAL to connect to the underlying RDBMS where the data is actually stored. If you are using one of the Jackalope backends, you can also specify options. They will be set on the Jackalope session. Currently this can be used to tune pre-fetching nodes by setting jackalope.fetch_depth to something bigger than 0.
Listing 22-1

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 backend: 5 # see below for how to configure the backend of your choice 6 workspace: default 7 username: admin 8 password: admin 9 ## tweak options for jackrabbit and doctrinedbal (all jackalope versions) 10 # options: 11 # 'jackalope.fetch_depth': 1

PHPCR Session with Jackalope Jackrabbit


The only setup required is to install Apache Jackrabbit (see installing Jackrabbit). The configuration needs the url parameter to point to your jackrabbit. Additionally you can tune some other jackrabbit-specific options, for example to use it in a load-balanced setup or to fail early for the price of some round trips to the backend.
Listing 22-2

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

# app/config/config.yml doctrine_phpcr: session: backend: type: jackrabbit url: http://localhost:8080/server/ ## jackrabbit only, optional. see https://github.com/jackalope/jackalope/blob/ master/src/Jackalope/RepositoryFactoryJackrabbit.php # default_header: ... # expect: 'Expect: 100-continue' # enable if you want to have an exception right away if PHPCR login fails # check_login_on_server: false # enable if you experience segmentation faults while working with binary data in documents # disable_stream_wrapper: true # enable if you do not want to use transactions and you neither want the odm to automatically use transactions

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 100

# its highly recommended NOT to disable transactions # disable_transactions: true

PHPCR Session with Jackalope Doctrine DBAL


This type uses Jackalope with a Doctrine database abstraction layer transport to provide PHPCR without any installation requirements beyond any of the RDBMS supported by Doctrine. You need to configure a Doctrine connection according to the DBAL section in the Symfony2 Doctrine documentation6.
Listing 22-3

1 2 3 4 5 6 7 8 9 10 11 12 13

# app/config/config.yml doctrine_phpcr: session: backend: type: doctrinedbal connection: doctrine.dbal.default_connection # enable if you want to have an exception right away if PHPCR login fails # check_login_on_server: false # enable if you experience segmentation faults while working with binary data in documents # disable_stream_wrapper: true # enable if you do not want to use transactions and you neither want the odm to automatically use transactions # its highly recommended NOT to disable transactions # disable_transactions: true

Once the connection is configured, you can create the database and you need to initialize the database with the doctrine:phpcr:init:dbal command.
Listing 22-4

1 $ php app/console doctrine:database:create 2 $ php app/console doctrine:phpcr:init:dbal

Of course, you can also use a different connection instead of the default. It is recommended to use a separate connection to a separate database if you also use Doctrine ORM or direct DBAL access to data, rather than mixing this data with the tables generated by jackalope-doctrinedbal. If you have a separate connection, you need to pass the alternate connection name to the doctrine:database:create command with the --connection option. For doctrine PHPCR commands, this parameter is not needed as you configured the connection to use.

PHPCR Session with Midgard2


Midgard2 is an application that provides a compiled PHP extension. It implements the PHPCR API on top of a standard RDBMS. To use the Midgard2 PHPCR provider, you must have both the midgard2 PHP extension7 and the midgard/phpcr package8 installed. The settings here correspond to Midgard2 repository parameters as explained in the getting started document9. The session backend configuration looks as follows:
6. http://symfony.com/doc/current/book/doctrine.html 7. http://midgard-project.org/midgard2/#download 8. http://packagist.org/packages/midgard/phpcr 9. http://midgard-project.org/phpcr/#getting_started

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 101

Listing 22-5

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 backend: 5 type: midgard2 6 db_type: MySQL 7 db_name: midgard2_test 8 db_host: "0.0.0.0" 9 db_port: 3306 10 db_username: "" 11 db_password: "" 12 db_init: true 13 blobdir: /tmp/cmf-blobs

For more information, please refer to the official Midgard PHPCR documentation10.

Doctrine PHPCR-ODM Configuration


This configuration section manages the Doctrine PHPCR-ODM system. If you do not configure anything here, the ODM services will not be loaded. If you enable auto_mapping, you can place your mappings in <Bundle>/Resources/config/doctrine/ <Document>.phpcr.xml resp. ...yml to configure mappings for documents you provide in the <Bundle>/Document folder. Otherwise you need to manually configure the mappings section. If auto_generate_proxy_classes is false, you need to run the cache:warmup command in order to have the proxy classes generated after you modified a document. You can also tune how and where to generate the proxy classes with the proxy_dir and proxy_namespace settings. The the defaults are usually fine here. You can also enable metadata caching11.
Listing 22-6

# app/config/config.yml doctrine_phpcr: odm: configuration_id: ~ auto_mapping: true mappings: <name>: mapping: true type: ~ dir: ~ alias: ~ prefix: ~ is_bundle: ~ auto_generate_proxy_classes: %kernel.debug% proxy_dir: %kernel.cache_dir%/doctrine/PHPCRProxies proxy_namespace: PHPCRProxies metadata_cache_driver: type: host: port: instance_class: class: id: array ~ ~ ~ ~ ~

10. http://midgard-project.org/phpcr/ 11. http://symfony.com/doc/master/reference/configuration/doctrine.html

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 102

Translation Configuration
If you are using multilingual documents, you need to configure the available languages. For more information on multilingual documents, see the PHPCR-ODM documentation on Multilanguage.
Listing 22-7

1 # app/config/config.yml 2 doctrine_phpcr: 3 odm: 4 ... 5 locales: 6 en: [e, fr] 7 de: [en, fr] 8 fr: [en, de]

This block defines the order of alternative locales to look up if a document is not translated to the requested locale.

General Settings
If the jackrabbit_jar path is set, you can use the doctrine:phpcr:jackrabbit console command to start and stop jackrabbit. You can tune the output of the doctrine:phpcr:dump command with dump_max_line_length.
Listing 22-8

1 # app/config/config.yml 2 doctrine_phpcr: 3 jackrabbit_jar: /path/to/jackrabbit.jar 4 dump_max_line_length: 120

Configuring Multiple Sessions


If you need more than one PHPCR backend, you can define sessions as child of the session information. Each session has a name and the configuration as you can use directly in session. You can also overwrite which session to use as default_session.
Listing 22-9

1 # app/config/config.yml 2 doctrine_phpcr: 3 session: 4 default_session: ~ 5 sessions: 6 <name>: 7 workspace: 8 username: 9 password: 10 backend: 11 # as above 12 options: 13 # as above

~ # Required ~ ~

If you are using the ODM, you will also want to configure multiple document managers. Inside the odm section, you can add named entries in the document_managers. To use the non-default session, specify the session attribute.
Listing 22-10

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 103

1 odm: 2 default_document_manager: ~ 3 document_managers: 4 <name>: 5 # same keys as directly in odm, see above. 6 session: <sessionname>

A full example looks as follows:


Listing 22-11

doctrine_phpcr: # configure the PHPCR sessions session: sessions: default: backend: %phpcr_backend% workspace: %phpcr_workspace% username: %phpcr_user% password: %phpcr_pass% website: backend: type: jackrabbit url: %magnolia_url% workspace: website username: %magnolia_user% password: %magnolia_pass% dms: backend: type: jackrabbit url: %magnolia_url% workspace: dms username: %magnolia_user% password: %magnolia_pass% # enable the ODM layer odm: document_managers: default: session: default mappings: SandboxMainBundle: ~ CmfContentBundle: ~ CmfMenuBundle: ~ CmfRoutingBundle: ~ website: session: website configuration_id: sandbox_magnolia.odm_configuration mappings: SandboxMagnoliaBundle: ~ dms: session: dms configuration_id: sandbox_magnolia.odm_configuration mappings: SandboxMagnoliaBundle: ~ auto_generate_proxy_classes: %kernel.debug%

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 104

This example also uses different configurations per repository (see the repository_id attribute). This case is explained in Using a Custom Document Class Mapper with PHPCR-ODM.

Services
You can access the PHPCR services like this:
Listing 22-12

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

<?php namespace Acme\DemoBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class DefaultController extends Controller { public function indexAction() { // ManagerRegistry instance with references to all sessions and document manager instances $registry = $this->container->get('doctrine_phpcr'); // PHPCR session instance $session = $this->container->get('doctrine_phpcr.default_session'); // PHPCR ODM document manager instance $documentManager = $this->container->get('doctrine_phpcr.odm.default_document_manager'); } }

Events
You can tag services to listen to Doctrine PHPCR-ODM events. It works the same way as for Doctrine ORM. The only differences are: use the tag name doctrine_phpcr.event_listener resp. doctrine_phpcr.event_subscriber instead of doctrine.event_listener. expect the argument to be of class Doctrine\ODM\PHPCR\Event\LifecycleEventArgs rather than in the ORM namespace. (this is subject to change, as doctrine commons 2.4 provides a common class for this event). You can register for the events as described in the PHPCR-ODM documentation12. Or you can tag your services as event listeners resp. event subscribers.
Listing 22-13

1 services: 2 my.listener: 3 class: Acme\SearchBundle\EventListener\SearchIndexer 4 tags: 5 - { name: doctrine_phpcr.event_listener, event: postPersist } 6 7 my.subscriber:

12. http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/events.html

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 105

8 9 10

class: Acme\SearchBundle\EventSubscriber\MySubscriber tags: - { name: doctrine_phpcr.event_subscriber }

Doctrine event subscribers (both ORM and PHPCR-ODM) can not return a flexible array of methods to call like the Symfony event subscriber13 can do. Doctrine event subscribers must return a simple array of the event names they subscribe to. Doctrine will then expect methods on the subscriber with the names of the subscribed events, just as when using an event listener.

More information with PHP code examples for the doctrine event system integration is in this Symfony cookbook entry14.

Constraint Validator
The bundle provides a ValidPhpcrOdm constraint validator you can use to check if your document Id or Nodename and Parent fields are correct.
Listing 22-14

1 # src/Acme/BlogBundle/Resources/config/validation.yml 2 Acme\BlogBundle\Entity\Author: 3 constraints: 4 - Doctrine\Bundle\PHPCRBundle\Validator\Constraints\ValidPhpcrOdm

Form Types
The bundle provides a couple of handy form types for PHPCR and PHPCR-ODM specific cases, along with form type guessers.

phpcr_odm_image
The phpcr_odm_image form maps to a document of type Doctrine\ODM\PHPCR\Document\Image and provides a preview of the uploaded image. To use it, you need to include the LiipImagineBundle15 in your project and define an imagine filter for thumbnails. This form type is only available if explicitly enabled in your application configuration by defining the imagine section under the odm section with at least enabled: true. You can also configure the imagine filter to use for the preview, as well as additional filters to remove from cache when the image is replaced. If the filter is not specified, it defaults to image_upload_thumbnail.
Listing 22-15

1 doctrine_phpcr: 2 # ... 3 odm: 4 imagine: 5 enabled: true 6 # filter: image_upload_thumbnail 7 # extra_filters: 8 # - imagine_filter_name1

13. http://symfony.com/doc/master/components/event_dispatcher/introduction.html#using-event-subscribers 14. http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html 15. https://github.com/liip/LiipImagineBundle/

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 106

9 # - imagine_filter_name2 10 11 # Imagine Configuration 12 liip_imagine: 13 # ... 14 filter_sets: 15 # define the filter to be used with the image preview 16 image_upload_thumbnail: 17 data_loader: phpcr 18 filters: 19 thumbnail: { size: [100, 100], mode: outbound }

Then you can add images to document forms as follows:


Listing 22-16

1 2 3 4 5 6 7 8

use Symfony\Component\Form\FormBuilderInterface; protected function configureFormFields(FormBuilderInterface $formBuilder) { $formBuilder ->add('image', 'phpcr_odm_image', array('required' => false)) ; }

If you set required to true for the image, the user must re-upload a new image each time he edits the form. If the document must have an image, it makes sense to require the field when creating a new document, but make it optional when editing an existing document. We are trying to make this automatic16.

Next you will need to add the fields.html.twig template from the DoctrinePHPCRBundle to the form.resources, to actually see the preview of the uploaded image in the backend.
Listing 22-17

1 # Twig Configuration 2 twig: 3 form: 4 resources: 5 - 'DoctrinePHPCRBundle:Form:fields.html.twig'

The document that should contain the Image document has to implement a setter method. To profit from the automatic guesser of the form layer, the name in the form element and this method name have to match:
Listing 22-18

1 public function setImage($image) 2 { 3 if (!$image) { 4 // this is normal and happens when no new image is uploaded 5 return; 6 } elseif ($this->image && $this->image->getFile()) { 7 // TODO: needed until this bug in PHPCRODM has been fixed: https://github.com/ 8 doctrine/phpcr-odm/pull/262 9 $this->image->getFile()->setFileContent($image->getFile()->getFileContent()); 10 } else { $this->image = $image;

16. https://groups.google.com/forum/?fromgroups=#!topic/symfony2/CrooBoaAlO4

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 107

11 12 }

To delete an image, you need to delete the document containing the image. (There is a proposal to improve the user experience for that in a DoctrinePHPCRBundle issue17.)
There is a doctrine listener to invalidate the imagine cache for the filters you specified. This listener will only operate when an Image is changed in a web request, but not when a CLI command changes images. When changing images with commands, you should handle cache invalidation in the command or manually remove the imagine cache afterwards.

phpcr_odm_reference_collection
This form type handles editing ReferenceMany collections on PHPCR-ODM documents. It is a choice field with an added referenced_class required option that specifies the class of the referenced target document. To use this form type, you also need to specify the list of possible reference targets as an array of PHPCRODM ids or PHPCR paths. The minimal code required to use this type looks as follows:
Listing 22-19

1 $dataArr = array( 2 '/some/phpcr/path/item_1' => 'first item', 3 '/some/phpcr/path/item_2' => 'second item', 4 ); 5 6 $formMapper 7 ->with('form.group_general') 8 ->add('myCollection', 'phpcr_odm_reference_collection', array( 9 'choices' => $dataArr, 10 'referenced_class' => 'Class\Of\My\Referenced\Documents', 11 )) 12 ->end();

When building an admin interface with Sonata Admin there is also the sonata_type_model that is more powerful, allowing to add to the referenced documents on the fly. Unfortunately it is currently broken18.

phpcr_reference
The phpcr_reference represents a PHPCR Property of type REFERENCE or WEAKREFERENCE within a form. The input will be rendered as a text field containing either the PATH or the UUID as per the configuration. The form will resolve the path or id back to a PHPCR node to set the reference. This type extends the text form type. It adds an option transformer_type that can be set to either path or uuid.

17. https://github.com/doctrine/DoctrinePHPCRBundle/issues/40 18. https://github.com/sonata-project/SonataDoctrineORMAdminBundle/issues/145

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 108

Repository Initializers
The Initializer is the PHPCR equivalent of the ORM schema tools. It is used to let bundles register PHPCR node types and to create required base paths in the repository. Initializers have to implement Doctrine\Bundle\PHPCRBundle\Initializer. If you don't need any special logic, you can simply configure the GenericInitializer as service and don't need to write any code. The generic initializer expects an array of base paths it will create if they do not exist, and an optional string defining namespaces and primary / mixin node types in the CND language. A service to use the generic initializer looks like this:
Listing 22-20

1 # src/Acme/ContentBundle/Resources/config/services.yml 2 acme.phpcr.initializer: 3 class: Doctrine\Bundle\PHPCRBundle\Initializer\GenericInitializer 4 arguments: 5 - { "%acme.content_basepath%", "%acme.menu_basepath%" } 6 - { "%acme.cnd%" } 7 tags: 8 - { name: "doctrine_phpcr.initializer" }

The doctrine:phpcr:repository:init command runs all tagged initializers.

Fixture Loading
To use the doctrine:phpcr:fixtures:load command, you additionally need to install the DoctrineFixturesBundle19 which brings the Doctrine data-fixtures into Symfony2. Fixtures work the same way they work for Doctrine ORM. You write fixture classes implementing Doctrine\Common\DataFixtures\FixtureInterface. If you place them in <Bundle>DataFixturesPHPCR, they will be auto detected if you specify no path to the fixture loading command. A simple example fixture class looks like this:
Listing 22-21

1 2 3 4 5 6 7 8 9 10 11 12 13 14

<?php namespace MyBundle\DataFixtures\PHPCR; use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\DataFixtures\FixtureInterface; class LoadMyData implements FixtureInterface { public function load(ObjectManager $manager) { // Create and persist your data here... } }

For more on fixtures, see the documentation of the DoctrineFixturesBundle20.

19. http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html 20. http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 109

Migration Loading
The DoctrinePHPCRBundle also ships with a simple command to run migration scripts. Migrations should implement the Doctrine\Bundle\PHPCRBundle\Migrator\MigratorInterface and registered as a service with a doctrine_phpcr.migrator tag contains an alias attribute uniquely identifying the migrator. There is an optional Doctrine\Bundle\PHPCRBundle\Migrator\AbstractMigrator class to use as a basis. To find out available migrations run:
Listing 22-22

1 $ php app/console doctrine:phpcr:migrator

Then pass in the name of the migrator to run it, optionally passing in an --identifier, --depth or -session argument. The later argument determines which session name to set on the migrator, while the first two arguments will simply be passed to the migrate() method. You can find an example migrator in the SimpleCmsBundle.

Doctrine PHPCR Commands


All commands about PHPCR are prefixed with doctrine:phpcr and you can use the --session argument to use a non-default session if you configured several PHPCR sessions. Some of these commands are specific to a backend or to the ODM. Those commands will only be available if such a backend is configured. Use app/console help <command> to see all options each of the commands has. doctrine:phpcr:workspace:create: Create a workspace in the configured repository; doctrine:phpcr:workspace:list: List all available workspaces in the configured repository; doctrine:phpcr:type:register: Register node types from a .cnd file in the PHPCR repository; doctrine:phpcr:type:list: List all node types in the PHPCR repository; doctrine:phpcr:purge: Remove a subtree or all content from the repository; doctrine:phpcr:repository:init: Register node types and create base paths. See above how to define custom initializers; doctrine:phpcr:fixtures:load: Load data fixtures to your PHPCR database; doctrine:phpcr:import: Import xml data into the repository, either in JCR system view format or arbitrary xml; doctrine:phpcr:export: Export nodes from the repository, either to the JCR system view format or the document view format; doctrine:phpcr:dump: Output all or some content of the repository; doctrine:phpcr:touch: Create or modify a node at the specified path; doctrine:phpcr:move: Move a node from one path to another; doctrine:phpcr:query: Execute a JCR SQL2 statement; doctrine:phpcr:mapping:info: Shows basic information about all mapped documents.
To use the doctrine:phpcr:fixtures:load command, you additionally need to install the DoctrineFixturesBundle21 and its dependencies. See that documentation page for how to use fixtures.

Jackrabbit Specific Commands


If you are using jackalope-jackrabbit, you also have a command to start and stop the jackrabbit server:
21. http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 110

jackalope:run:jackrabbit Start and stop the Jackrabbit server

Doctrine DBAL Specific Commands


If you are using jackalope-doctrine-dbal, you have a command to initialize the database: jackalope:init:dbal Prepare the database for Jackalope Doctrine DBAL Note that you can also use the doctrine dbal command to create the database.

Some Example Command Runs


Running SQL2 queries22 against the repository:
Listing 22-23

1 $ php app/console doctrine:phpcr:query "SELECT title FROM [nt:unstructured] WHERE NAME() = 'home'"

Dumping nodes under /cms/simple including their properties:


Listing 22-24

1 $ php app/console doctrine:phpcr:dump /cms/simple --props

22. http://www.h2database.com/jcr/grammar.html

PDF brought to you by generated on July 18, 2013

Chapter 22: DoctrinePHPCRBundle | 111

Chapter 23

MenuBundle
The MenuBundle1 provides menus from a doctrine object manager with the help of KnpMenuBundle2.

Dependencies
This bundle is extending the KnpMenuBundle3. Unless you change defaults and provide your own implementations, this bundle also depends on SymfonyRoutingBundle for the router service cmf_routing.dynamic_router. Note that you need to explicitly enable the dynamic router as per default it is not loaded. See the documentation of the cmf routing bundle for how to do this. PHPCR-ODM to load route documents from the content repository

Configuration
If you want to use default configurations, you do not need to change anything. The values are:
Listing 23-1

1 cmf_menu: 2 menu_basepath: /cms/menu 3 document_manager_name: default 4 admin_class: ~ 5 document_class: ~ 6 content_url_generator: router 7 route_name: ~ # cmf routes are created by content instead of name 8 content_basepath: ~ # defaults to cmf_core.content_basepath 9 allow_empty_items: ~ # defaults to false 10 voters: 11 uri_prefix: false # enable the UriPrefixVoter for current menu item 12 content_identity: not set # enable the RequestContentIdentityVoter

1. https://github.com/symfony-cmf/MenuBundle#readme 2. https://github.com/knplabs/KnpMenuBundle 3. https://github.com/knplabs/KnpMenuBundle

PDF brought to you by generated on July 18, 2013

Chapter 23: MenuBundle | 112

13 14 15 16 17 18 19

content_key: not set # override DynamicRouter::CONTENT_KEY use_sonata_admin: auto # use true/false to force using / not using sonata admin multilang: # the whole multilang section is optional use_sonata_admin: auto # use true/false to force using / not using sonata admin admin_class: ~ document_class: ~ locales: [] # if you use multilang, you have to define at least one locale

If you want to render the menu from twig, make sure you have not disabled twig in the knp_menu configuration section. If sonata-project/doctrine-phpcr-admin-bundle is added to the composer.json require section, the menu documents are exposed in the SonataDoctrinePhpcrAdminBundle. For instructions on how to configure this Bundle see SonataDoctrinePhpcrAdminBundle. By default, use_sonata_admin is automatically set based on whether SonataDoctrinePhpcrAdminBundle is available but you can explicitly disable it to not have it even if sonata is enabled, or explicitly enable to get an error if Sonata becomes unavailable. By default, menu nodes that have neither the uri nor the route field set and no route can be generated from the content they link to are skipped by the ContentAwareFactory. This also leads to their descendants not showing up. If you want to generate menu items without a link instead, set the allow_empty_items parameter to true to make the menu items show up as static text instead.

Menu Entries
A MenuItem document defines menu entries. You can build menu items based on symfony routes, absolute or relative urls or referenceable PHPCR-ODM content documents. The menu tree is built from documents under [menu_basepath]/[menuname]. You can use different document classes for menu items as long as they implement Knp\Menu\NodeInterface to integrate with KnpMenuBundle. The default MenuNode document discards children that do not implement this interface. Examples:
Listing 23-2

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

<?php

// get document manager $dm = ...; $rootNode = $dm->find(null, ...); // retrieve parent menu item // using referenceable content document $blogContent = $dm->find(null, '/my/cms/content/blog');
$blogNode = new MenuNode(); $blogNode->setName('blog'); $blogNode->setParent($parent); $blogNode->setContent($blogDocument); $blogNode->setLabel('Blog'); $dm->persist($blogNode);

// using a route document $timelineRoute = $dm->find(null, '/my/cms/routes/timeline');

PDF brought to you by generated on July 18, 2013

Chapter 23: MenuBundle | 113

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

$timelineNode = new MenuNode(); $timelineNode->setContent($timelineRoute); // ... $dm->persist($timelineNode);

// using a symfony route $sfRouteNode = new MenuNode(); $sfRouteNode->setRoute('my_hard_coded_symfony_route'); // ...


$dm->persist($sfRouteNode);

// using URL $urlNode = new MenuNode(); $urlNode->setUri('http://www.example.com'); // ...


$dm->persist($urlNode); $dm->flush();

By default content documents are created using a weak reference (this means you will be able to delete the referenced content). You can specify a strong reference by using setWeak(false):
Listing 23-3

1 2 3 4 5

<?php $node = new MenuNode; // ... $node->setWeak(false);

When content is referenced weakly and subsequently deleted the rendered menu will not provide a link to the content.

Current Menu Item


A menu item can be the current item. If it is the current item, this information is passed to the twig template which by default adds the css class current and all menu items that are ancestors of that item get the class current_ancestor. This will typically used in CSS to highlight menu entries. The decision about being current item happens by comparing the URI associated with the menu item with the request URI. Additionally, the CMF menu bundle supports voters that can look at the MenuItem and optionally the request. There are two voter services configured but not enabled by default, another voter that you can use to configure services and you can write your own voter classes.
The CMF MenuBundle is based on Knp Menu 1.x. The 2.0 rewrite of Knp Menu will add current item voters in the core Knp library. The CMF bundle voters are interface compatible and follow the same mechanism as Knp Menu to ease upgrading.

PDF brought to you by generated on July 18, 2013

Chapter 23: MenuBundle | 114

RequestContentIdentityVoter
This voter looks at the content field of the menu item extras and compares it with the main content attribute of the request. The name for the main content attribute in the request is configurable with the content_key option - if not set it defaults to the constant DynamicRouter::CONTENT_KEY. You can enable this voter by setting cmf_menu.voters.content_identity to ~ in your config.yml to use a custom content_key for the main content attribute name, set it explicitly:
Listing 23-4

1 cmf_menu: 2 voters: 3 content_identity: 4 content_key: myKey

UriPrefixVoter
The uri prefix voter looks at the content field of the menu item extras and checks if it contains an instance of the symfony Route class. If so, it compares the route option currentUriPrefix with the request URI. This allows you to make a whole sub-path of your site trigger the same menu item as current, but you need to configure the prefix option on your route documents. To enable the prefix voter, set the configuration key cmf_menu.voters.uri_prefix: ~.

RequestParentContentIdentityVoter
This voter has the same logic of looking for a request attribute to get the current content, but calls getParent on it. This parent is compared to the content of the menu item extras. That way, content that does not have its own menu entry but a parent that does have a menu item can trigger the right menu entry to be highlighted. To use this voter you need to configure a custom service with the name of the content in the request and your model class to avoid calling getParent on objects that do not have that method. You need to tag the service as cmf_menu.voter and also as cmf_request_aware because it depends on the request. The service looks the same as for complete custom voters (see below), except you do not need to write your own PHP code:
Listing 23-5

services: my_bundle.menu_voter.parent: class: Symfony\Cmf\Bundle\MenuBundle\Voter\RequestParentContentIdentityVoter arguments: - contentDocument - %my_bundle.my_model_class% tags: - { name: "cmf_menu.voter" } - { name: "cmf_request_aware" }

Custom Voter
Voters must implement the Symfony\Cmf\MenuBundle\Voter\VoterInterface. To make the menu bundle notice the voter, tag it with cmf_menu.voter. If the voter needs the request, add the tag cmf_request_aware to have a listener calling setRequest on the voter before it votes for the first time. If you need to know the content the menu item points to, look in the content field of the menu item extras: $item->getExtra('content');. The ContentAwareFactory places the content referenced by the route there - if it does reference a content. Your voter should handle the case where the content is null. For an example service definition see the section above for RequestParentIdentityVoter. A voter will look something like this:
PDF brought to you by generated on July 18, 2013 Chapter 23: MenuBundle | 115

Listing 23-6

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

<?php namespace Acme\MenuBundle\Voter; use Symfony\Cmf\Bundle\MenuBundle\Voter\VoterInterface; use Knp\Menu\ItemInterface; class MyVoter implements VoterInterface { private $request; public function setRequest(Request $request) { $this->request = $request; }

/** * {@inheritDoc} */ public function matchItem(ItemInterface $item) { if (...) { // $item is the current menu item return true; } if (...) { // $item for sure is NOT the current menu item // even if other voters might match return false; } // we dont know if this is the current item return null;
} }

Rendering Menus
Adjust your twig template to load the menu.
Listing 23-7

1 {{ knp_menu_render('simple') }}

The menu name is the name of the node under menu_basepath. For example if your repository stores the menu nodes under /cms/menu , rendering "main" would mean to render the menu that is at /cms/menu/ main

How to use Non-Default Other Components


If you use the cmf menu with PHPCR-ODM, you just need to store Route documents under menu_basepath. If you use a different object manager, you need to make sure that the menu root document is found with:
Listing 23-8

1 $dm->find($menu_document_class, $menu_basepath . $menu_name)

PDF brought to you by generated on July 18, 2013

Chapter 23: MenuBundle | 116

The route document must implement Knp\Menu\NodeInterface - see MenuNode document for an example. You probably need to specify menu_document_class too, as only PHPCR-ODM can determine the document from the database content. If you use the cmf menu with the DynamicRouter, you need no route name as the menu document just needs to provide a field content_key in the options. If you want to use a different service to generate URLs, you need to make sure your menu entries provide information in your selected content_key that the url generator can use to generate the url. Depending on your generator, you might need to specify a route_name too. Note that if you just want to generate normal symfony routes with a menu that is in the database, you can pass the core router service as content_url_generator, make sure the content_key never matches and make your menu documents provide the route name and eventual routeParameters.

Publish Workflow Interface


Menu nodes implement the write interfaces for publishable and publish time period, see the publish workflow documentation for more information.

PDF brought to you by generated on July 18, 2013

Chapter 23: MenuBundle | 117

Chapter 24

RoutingBundle
The RoutingBundle1 integrates dynamic routing into Symfony using Routing. The ChainRouter is meant to replace the default Symfony Router. All it does is collect a prioritized list of routers and try to match requests and generate URLs with all of them. One of the routers in that chain can of course be the default router so you can still use the standard way for some of your routes. Additionally, this bundle delivers useful router implementations. Currently, there is the DynamicRouter that routes based on a custom loader logic for Symfony2 Route objects. The provider can be implemented using a database, for example with Doctrine PHPCR-ODM2 or Doctrine ORM. This bundle provides a default implementation for Doctrine PHPCR-ODM3. The DynamicRouter service is only made available when explicitly enabled in the application configuration. Finally this bundles provides route documents for Doctrine PHPCR-ODM4 and a controller for redirection routes.

Dependencies
Symfony CMF routing5

ChainRouter
The ChainRouter can replace the default symfony routing system with a chain- enabled implementation. It does not route anything on its own, but only loops through all chained routers. To handle standard configured symfony routes, the symfony default router can be put into the chain.

1. https://github.com/symfony-cmf/RoutingBundle#readme 2. https://github.com/doctrine/phpcr-odm 3. https://github.com/doctrine/phpcr-odm 4. https://github.com/doctrine/phpcr-odm 5. https://github.com/symfony-cmf/Routing#readme

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 118

Configuration
In your app/config/config.yml, you can specify which router services you want to use. If you do not specify the routers_by_id map at all, by default the chain router will just load the built-in symfony router. When you specify the routers_by_id list, you need to have an entry for router.default if you want the Symfony2 router (that reads the routes from app/config/routing.yml). The format is service_name: priority - the higher the priority number the earlier this router service is asked to match a route or to generate a url
Listing 24-1

1 # app/config/config.yml 2 cmf_routing: 3 chain: 4 routers_by_id: 5 # enable the DynamicRouter with high priority to allow overwriting configured 6 routes with content 7 cmf_routing.dynamic_router: 200 8 # enable the symfony default router with a lower priority 9 router.default: 100 10 # whether the chain router should replace the default router. defaults to true 11 # if you set this to false, the router is just available as service 12 # cmf_routing.router and you need to do something to trigger it # replace_symfony_router: true

Loading Routers with Tagging


Your routers can automatically register, just add it as a service tagged with router and an optional priority. The higher the priority, the earlier your router will be asked to match the route. If you do not specify the priority, your router will come last. If there are several routers with the same priority, the order between them is undetermined. The tagged service will look like this
Listing 24-2

services: my_namespace.my_router: class: %my_namespace.my_router_class% tags: - { name: router, priority: 300 }

See also official Symfony2 documentation for DependencyInjection tags6

Dynamic Router
This implementation of a router uses the NestedMatcher which loads routes from a RouteProviderInterface. The provider interface can be easily implemented with Doctrine. The router works with extended UrlMatcher and UrlGenerator classes that add loading routes from the database and the concept of referenced content. The NestedMatcher service is set up with a route provider. See the configuration section for how to change the route_repository_service and the following section on more details for the default PHPCRODM7 based implementation. You may want to configure route enhancers to decide what controller is used to handle the request, to avoid hard coding controller names into your route documents.

6. http://symfony.com/doc/2.1/reference/dic_tags.html 7. https://github.com/doctrine/phpcr-odm

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 119

The minimum configuration required to load the dynamic router as service cmf_routing.dynamic_router is to have enabled: true in your config.yml (the router is automatically enabled as soon as you add any other configuration to the dynamic entry). Without enabling it, the dynamic router service will not be loaded at all, allowing you to use the ChainRouter with your own routers
Listing 24-3

1 # app/config/config.yml 2 cmf_routing: 3 dynamic: 4 enabled: true

PHPCR-ODM integration
This bundle comes with a route repository implementation for PHPCR-ODM8. PHPCR is well suited to the tree nature of the data. If you use PHPCR-ODM9 with a route document like the one provided, you can just leave the repository service at the default. The default repository loads the route at the path in the request and all parent paths to allow for some of the path segments being parameters. If you need a different way to load routes or for example never use parameters, you can write your own repository implementation to optimize (see cmf_routing.xml for how to configure the service).

Match Process
Most of the match process is described in the documentation of the CMF Routing component. The only difference is that the bundle will place the contentDocument in the request attributes instead of the route defaults. Your controllers can (and should) declare the parameter $contentDocument in their Action methods if they are supposed to work with content referenced by the routes. See Symfony\Cmf\Bundle\ContentBundle\Controller\ContentController for an example.

Configuration
To configure what controller is used for which content, you can specify route enhancers. Presence of each of any enhancer configuration makes the DI container inject the respective enhancer into the DynamicRouter. The possible enhancements are (in order of precedence): (Explicit controller): If there is a _controller set in getRouteDefaults(), no enhancer will overwrite it. Explicit template: requires the route document to return a '_template' parameter in getRouteDefaults. The configured generic controller is set by the enhancer. Controller by alias: requires the route document to return a 'type' value in getRouteDefaults() Controller by class: requires the route document to return an object for getRouteContent(). The content document is checked for being instanceof the class names in the map and if matched that controller is used. Instanceof is used instead of direct comparison to work with proxy classes and other extending classes. Template by class: requires the route document to return an object for getRouteContent(). The content document is checked for being instanceof the class names in the map and if

8. https://github.com/doctrine/phpcr-odm 9. https://github.com/doctrine/phpcr-odm

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 120

matched that template will be set as '_template' in the $defaults and the generic controller used as controller.
Listing 24-4

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

# app/config/config.yml cmf_routing: dynamic: generic_controller: cmf_content.controller:indexAction controllers_by_type: editablestatic: sandbox_main.controller:indexAction controllers_by_class: Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent: cmf_content.controller:indexAction templates_by_class: Symfony\Cmf\Bundle\ContentBundle\Document\StaticContent: CmfContentBundle:StaticContent:index.html.twig # the route provider is responsible for loading routes. manager_registry: doctrine_phpcr manager_name: default # if you use the default doctrine route repository service, you # can use this to customize the root path for the `PHPCR-ODM`_ # RouteProvider. This base path will be injected by the # Listener\IdPrefix - but only to routes matching the prefix, # to allow for more than one route source. routing_repositoryroot: /cms/routes # If you want to replace the default route provider or content repository # you can specify their service IDs here. route_provider_service_id: my_bundle.provider.endpoint content_repository_service_id: my_bundle.repository.endpoint # an orm provider might need different configuration. look at # cmf_routing.xml for an example if you need to define your own # service

To see some examples, please look at the CMF sandbox10 and specifically the routing fixtures loading.
You can also define your own RouteEnhancer classes for specific use cases. See Customize.

Using the PHPCR-ODM route document


All route classes must extend the Symfony core Route class. The documents can either be created by code (for example a fixtures script) or with a web interface like the one provided for Sonata PHPCR-ODM admin (see below). PHPCR-ODM maps all features of the core route to the storage, so you can use setDefault, setRequirement, setOption and setHostnamePattern like normal. Additionally when creating a route, you can define whether .{_format} should be appended to the pattern and configure the required _format with a requirements. The other constructor option lets you control whether the route should append a trailing slash because this can not be expressed with a PHPCR name. The default is to have no trailing slash.
10. https://github.com/symfony-cmf/cmf-sandbox

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 121

All routes are located under a configured root path, for example '/cms/routes'. A new route can be created in PHP code as follows:
Listing 24-5

1 2 3 4 5 6

use Symfony\Cmf\Bundle\RoutingBundle\Document\Route; $route = new Route; $route->setParent($dm->find(null, '/routes')); $route->setName('projects'); // set explicit controller (both service and Bundle:Name:action syntax work) $route->setDefault('_controller', 'sandbox_main.controller:specialAction');

The above example should probably be done as a route configured in a Symfony xml/yml file however, unless the end user is supposed to change the URL or the controller. To link a content to this route, simply set it on the document.
Listing 24-6

1 $content = new Content('my content'); // Content must be a mapped class 2 $route->setRouteContent($content);

This will put the document into the request parameters and if your controller specifies a parameter called $contentDocument, it will be passed this document. You can also use variable patterns for the URL and define requirements and defaults.
Listing 24-7

1 2 3 4

// do not forget leading slash if you want /projects/{id} and not /projects{id} $route->setVariablePattern('/{id}'); $route->setRequirement('id', '\d+'); $route->setDefault('id', 1);

This will give you a route that matches the URL /projects/<number> but also /projects as there is a default for the id parameter. This will match /projects/7 as well as /projects but not /projects/ x-4. The document is still stored at /routes/projects. This will work because, as mentioned above, the route provider will look for route documents at all possible paths and pick the first that matches. In our example, if there is a route document at /routes/projects/7 that matches (no further parameters) it is selected. Otherwise we check if /routes/projects has a pattern that matches. If not, the top document at /routes is checked. Of course you can also have several parameters, like with normal Symfony routes. The semantics and rules for patterns, defaults and requirements are exactly the same as in core routes. Your controller can expect the $id parameter as well as the $contentDocument as we set a content on the route. The content could be used to define an intro section that is the same for each project or other shared data. If you don't need content, you can just not set it in the document.

Sonata Admin Configuration


If sonata-project/doctrine-phpcr-admin-bundle is added to the composer.json require section and the SonataDoctrinePhpcrAdminBundle is loaded in the application kernel, the route documents are exposed in the SonataDoctrinePhpcrAdminBundle. For instructions on how to configure this Bundle see SonataDoctrinePhpcrAdminBundle. By default, use_sonata_admin is automatically set based on whether SonataDoctrinePhpcrAdminBundle is available, but you can explicitly disable it to not have it even if sonata is enabled, or explicitly enable to get an error if Sonata becomes unavailable. If you want to use the admin, you want to configure the content_basepath to point to the root of your content documents.
Listing 24-8

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 122

1 # app/config/config.yml 2 cmf_routing: 3 use_sonata_admin: auto # use true/false to force using / not using sonata admin 4 content_basepath: ~ # used with sonata admin to manage content, defaults to cmf_core.content_basepath

Form Type
The bundle defines a form type that can be used for classical "accept terms" checkboxes where you place urls in the label. Simply specify cmf_routing_terms_form_type as the form type name and specify a label and an array with content_ids in the options:
Listing 24-9

1 $form->add('terms', 'cmf_routing_terms_form_type', array( 2 'label' => 'I have seen the <a href="%team%">Team</a> and <a href="%more%">More</a> 3 pages ...', 4 'content_ids' => array('%team%' => '/cms/content/static/team', '%more%' => '/cms/ content/static/more') ));

The form type automatically generates the routes for the specified content and passes the routes to the trans twig helper for replacement in the label.

Further notes
See the documentation of the CMF Routing component11 for information on the RouteObjectInterface, redirections and locales. Notes: RouteObjectInterface: The provided documents implement this interface to map content to routes and to (optional) provide a custom route name instead of the symfony core compatible route name. Redirections: This bundle provides a controller to handle redirections.
Listing 24-10

1 # app/config/config.yml 2 cmf_routing: 3 controllers_by_class: 4 Symfony\Cmf\Component\Routing\RedirectRouteInterface: cmf_routing.redirect_controller:redirectAction

Customize
You can add more RouteEnhancerInterface implementations if you have a case not handled by the provided ones. Simply define services for your enhancers and tag them with dynamic_router_route_enhancer to have them added to the routing.

11. https://github.com/symfony-cmf/Routing

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 123

If you use an ODM / ORM different to PHPCR-ODM12, you probably need to specify the class for the route entity (in PHPCR-ODM13, the class is automatically detected). For more specific needs, have a look at DynamicRouter and see if you want to extend it. You can also write your own routers to hook into the chain.

Learn more from the Cookbook


Using a Custom Route Repository with Dynamic Router

Further notes
For more information on the Routing component of Symfony CMF, please refer to: Routing for an introductory guide on Routing bundle Routing for most of the actual functionality implementation Symfony2's Routing14 component page

12. https://github.com/doctrine/phpcr-odm 13. https://github.com/doctrine/phpcr-odm 14. http://symfony.com/doc/current/components/routing/introduction.html

PDF brought to you by generated on July 18, 2013

Chapter 24: RoutingBundle | 124

Chapter 25

RoutingAutoBundle
The RoutingAutoBundle allows you to define automatically created routes for documents. This implies a separation of the Route and Content documents. If your needs are simple this bundle may not be for you and you should have a look at SimpleCmsBundle. For the sake of example, we will imagine a blog application that has two routeable contents, the blog itself, and the posts for the blog. We will call these documents Blog and Post, and we will class them as content documents.
In our example we add an auto route for the blog, but in reality, as a blog is something you create rarely, you will probably want to create routes for your blog manually, but its up to you.

If we create a new Blog with the title "My New Blog" the bundle could automatically create the route /blogs/my-new-blog. For each new Post it could create a route such as /blogs/my-new-blog/myposts-title. This URL resolves to a special type of route that we will call the auto route. By default, when we update a content document that has an auto route the corresponding auto route will also be updated, when deleting a content document the corresponding auto route will also be deleted. If required, the bundle can also be configured to do extra stuff, like, for example, leaving a RedirectRoute when the location of a content document changes or automatically displaying an index page when an unconfigured intermediate path is accessed (for example, listing all the children under /blogs instead of returning a 404).

Why not simply use a single route?


Of course, our fictional blog application could use a single route with a pattern /blogs/my-new-blog/ {slug} which could be handled by a controller. Why not just do this? 1. By having a route for each page in the system the application has a knowledge of which URLs are accessible, this can be very useful, for example, when specifying endpoints for menu items are generating a site map.

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 125

2. By separating the route from the content we allow the route to be customized independently of the content, for example, a blog post may have the same title as another post but might need a different URL. 3. Separate route documents are translateable - this means we can have a URL for each language, "/welcome" and "/bienvenue" would each reference the same document in English and French respectively. This would be difficult if the slug were embedded in the content document. 4. By decoupling route and content the application doesn't care what is referenced in the route document. This means that we can easily replace the class of document referenced.

Anatomy of an automatic URL


The diagram below shows a fictional URL for a blog post. The first 6 elements of the URL are what we will call the content path. The last element we will call the content name. ../../_images/routing_auto_post_schema.png The content path is further broken down into route stacks and routes. A route stack is a group of routes and routes are simply documents in the PHPCR tree.
Although routes in this case can be of any document class, only objects which extend the Route1 object will be considered when matching a URL. The default behavior is to use Generic documents when generating a content path, and these documents will result in a 404 when accessed directly.

Internally each route stack is built up by a builder unit. Builder units contain one path provider class and two actions classes one action to take if the provided path exists in the PHPCR tree, the other if it does not. The goal of each builder unit is to generate a path and then provide a route object for each element in that path. The configuration for the example above could be as follows:
Listing 25-1

1 cmf_routing_auto: 2 3 auto_route_mapping: 4 My\Namespace\Bundle\BlogBundle\Document\Post: 5 content_path: 6 # corresponds first route stack in diagram: my, blog, my-blog 7 blog_path: 8 provider: 9 name: content_object 10 method: getBlog 11 exists_action: 12 strategy: use 13 not_exists_action: 14 strategy: throw_exception 15 16 # corresponds to second route stack: 2013,04,06 17 date: 18 provider: 19 name: content_datetime

1. http://api.symfony.com/master/Symfony/Component/Routing/Route.html

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 126

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

method: getPublishedDate exists_action: strategy: use not_exists_action: strategy: create

# corresponds to the content name: My Post Title content_name: provider: name: content_method method: getTitle exists_action: strategy: auto_increment pattern: -%d not_exists_action: strategy: create

The Post document would then need to implement the methods named above as follows:
Listing 25-2

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

<?php class Post { public function getBlog() { // return the blog object associated with the post return $this->blog; } public function getPublishedDate() { return new \DateTime('2013/04/06'); } public function getTitle() { return "My post title"; } }

Path Providers
Path providers specify a target path which is used by the subsequent path actions to provide the actual route documents. Base providers must be the first configured as the first builder in the content path chain. This is because the paths that they provide correspond directly to an existing path, i.e. they have an absolute reference.

specified (base provider)


This is the most basic path provider and allows you to specify an exact (fixed) path.
Listing 25-3

1 path_provider: 2 name: specified 3 path: this/is/a/path

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 127

Options: path - required The path to provide.


You never specifiy absolute paths in the auto route system. If the builder unit is the first content path chain it is understood that it is the base of an absolute path.

content_object (base provider)


The content object provider will try and provide a path from an object implementing RouteAwareInterface provided by a designated method on the content document. For example, if you have a Post class, which has a getBlog method, using this provider you can tell the Post auto route to use the route of the blog as a base. So basically, if your blog content has a path of /this/is/my/blog you can use this path as the base of your Post auto-route. Example:
Listing 25-4

1 provider: 2 name: content_object 3 method: getBlog

At the time of writing translated objects are not supported. This isn't hard to do, but well, I just havn't done it yet.

Options: method: required Method used to return the document whose route path we wish to use.

content_method
The content_method provider allows the content object (e.g. a blog Post) to specify a path using one of its methods. This is quite a powerful method as it allows the content document to do whatever it can to produce the route, the disadvantage is that your content document will have extra code in it. Example 1:
Listing 25-5

1 path_provider: 2 name: content_method 3 method: getTitle

This example will use the existing method "getTitle" of the Post document to retrieve the title. By default all strings are slugified. The method can return the path either as a single string or an array of path elements as shown in the following example:
Listing 25-6

1 <?php 2 3 class Post

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 128

4 { 5 6 7 8 9 10 11 12 13 14 }

public function getTitle() { return "This is a post"; } public function getPathElements() { return array('this', 'is', 'a', 'path'); }

Options: method: required Method used to return the route name/path/path elements. slugify: If we should use the slugifier, default is true.

content_datetime
The content_datettime provider will provide a path from a DateTime object provided by a designated method on the content document. Example 1:
Listing 25-7

1 provider: 2 name: content_datetime 3 method: getDate

Example 2:
Listing 25-8

1 provider: 2 name: content_datetime 3 method: getDate 4 date_format: Y/m/d

This method extends content_method and inherits the slugify feature. Internally we return a string using the DateTime->format() method. This means that you can specify your date in anyway you like and it will be automatically slugified, also, by adding path separators in the date_format you are effectively creating routes for each date component as slugify applies to each element of the path.

Options: method: required Method used to return the route name/path/path elements. slugify: If we should use the slugifier, default is true. date_format: Any date format accepted by the DateTime class, default Y-m-d.

Path Exists Actions


These are the default actions available to take if the path provided by a path_provider already exists and so creating a new path would create a conflict.

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 129

auto_increment
The auto_increment action will add a numerical suffix to the path, for example my/path would first become my/path-1 and if that path also exists it will try my/path-2, my/path-3 and so on into infinity until it finds a path which doesn't exist. This action should typically be used in the content_name builder unit to resolve conflicts. Using it in the content_path builder chain would not make much sense (I can't imagine any use cases at the moment). Example:
Listing 25-9

1 exists_action: 2 name: auto_increment

use
The use action will simply take the existing path and use it. For example, in our post example the first builder unit must first determine the blogs path, /my/blog, if this path exists (and it should) then we will use it in the stack. This action should typically be used in one of the content path builder units to specify that we should use the existing route, on the other hand, using this as the content name builder action should cause the old route to be overwritten. Example:
Listing 25-10

1 exists_action: 2 name: use

Path not Exists Actions


These are the default actions available to take if the path provided by a path_provider does not exist.

create
The create action will create the path. currently all routes provided by the content path build units will be created as Generic documents, whilst the content name route will be created as an AutoRoute document.
Listing 25-11

1 not_exists_action: 2 name: create

throw_exception
This action will throw an exception if the route provided by the path provider does not exist. You should take this action if you are sure that the route should exist.
Listing 25-12

1 not_exists_action: 2 name: create

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 130

Customization
Adding Path Providers
The goal of a PathProvider class is to add one or several path elements to the route stack. For example, the following provider will add the path foo/bar to the route stack:
Listing 25-13

1 2 3 4 5 6 7 8 9 10 11 12

<?php use Symfony\Cmf\Bundle\RoutingAutoBundle\AutoRoute\PathProviderInterface; use Symfony\Cmf\Bundle\RoutingAutoBundle\AutoRoute\RouteStack; class FoobarProvider implements PathProviderInterface { public function providePath(RouteStack $routeStack) { $routeStack->addPathElements(array('foo', 'bar')); } }

To use the path provider you must register it in the DIC and add the cmf_routing_auto.provider tag and set the alias accordingly.
Listing 25-14

1 <service 2 id="my_cms.some_bundle.path_provider.foobar" 3 class="FoobarProvider" 4 scope="prototype" 5 > 6 <tag name="cmf_routing_auto.provider" alias="foobar"/> 7 </service>

The foobar path provider is now available as foobar.


The that both path providers and path actions need to be defined with a scope of "prototype". This ensures that each time the auto routing system requests the class a new one is given and we do not have any state problems.

Adding Path Actions


In the auto routing system, a "path action" is an action to take if the path provided by the "path provider" exists or not. You can add a path action by extending the PathActionInterface and registering your new class correctly in the DI configuration. This is a very simple implementation from the bundle - it is used to throw an exception when a path already exists:
Listing 25-15

1 2 3 4 5 6 7

<?php namespace Symfony\Cmf\Bundle\RoutingAutoBundle\AutoRoute\PathNotExists; use Symfony\Cmf\Bundle\RoutingAutoBundle\AutoRoute\PathActionInterface; use Symfony\Cmf\Bundle\RoutingAutoBundle\AutoRoute\Exception\CouldNotFindRouteException; use Symfony\Cmf\Bundle\RoutingAutoBundle\AutoRoute\RouteStack;

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 131

8 9 class ThrowException implements PathActionInterface 10 { 11 public function init(array $options) 12 { 13 } 14 15 public function execute(RouteStack $routeStack) 16 { 17 throw new CouldNotFindRouteException('/'.$routeStack->getFullPath()); 18 } 19 }

It is registered in the DI configuration as follows:


Listing 25-16

1 <service 2 id="my_cms.not_exists_action.throw_exception" 3 class="My\Cms\AutoRoute\PathNotExists\ThrowException" 4 scope="prototype" 5 > 6 <tag name="cmf_routing_auto.not_exists_action" alias="throw_exception"/> 7 </service>

Note the following: Scope: Must always be set to prototype; Tag: The tag registers the service with the auto routing system, it can be one of the following; cmf_routing_auto.exists.action - if the action is to be used when a path exists; cmf_routing_auto.not_exists.action - if the action is to be used when a path does not exist; Alias: The alias of the tag is the name by which you will reference this action in the auto routing schema.

PDF brought to you by generated on July 18, 2013

Chapter 25: RoutingAutoBundle | 132

Chapter 26

SearchBundle
The SearchBundle1 provides integration with LiipSearchBundle2 to provide a site wide search.

Dependencies
LiipSearchBundle3

Configuration
The configuration key for this bundle is cmf_search
Listing 26-1

1 # app/config/config.yml 2 cmf_search: 3 document_manager_name: default 4 translation_strategy: child # can also be set to an empty string or attribute 5 translation_strategy: attribute 6 search_path: /cms/content 7 search_fields: 8 title: title 9 summary: body

1. https://github.com/symfony-cmf/SearchBundle#readme 2. https://github.com/liip/LiipSearchBundle 3. https://github.com/liip/LiipSearchBundle

PDF brought to you by generated on July 18, 2013

Chapter 26: SearchBundle | 133

Chapter 27

SimpleCmsBundle
The SimpleCmsBundle1 provides a simplistic CMS on top of the CMF components and bundles. While the core CMF components focus on flexibility, the simple CMS trades away some of that flexibility in favor of simplicity. The SimpleCmsBundle provides a solution to easily map content, routes and menu items based on a single tree structure in the content repository. For a simple example installation of the bundle check out the Symfony CMF Standard Edition2 You can find an introduction to the bundle in the Getting started3 section. The CMF website4 is another application using the SimpleCmsBundle.

Dependencies
As specified in the bundle composer.json this bundle depends on most CMF bundles.

Configuration
The configuration key for this bundle is cmf_simple_cms The use_menu option automatically enables a service to provide menus out of the simple cms if the MenuBundle is enabled. You can also explicitly disable it if you have the menu bundle but do not want to use the default service, or explicitly enable to get an error if the menu bundle becomes unavailable. The routing section is configuring what template or controller to use for a content class. This is reusing what the cmf routing bundle does, please see the corresponding routing configuration section. It also explains the generic_controller. See the section below for multilanguage support.

1. https://github.com/symfony-cmf/SimpleCmsBundle#readme 2. https://github.com/symfony-cmf/symfony-cmf-standard 3. #cmf-getting_started-simplecms 4. https://github.com/symfony-cmf/cmf-website/

PDF brought to you by generated on July 18, 2013

Chapter 27: SimpleCmsBundle | 134

Listing 27-1

1 # app/config/config.yml 2 cmf_simple_cms: 3 use_menu: auto # use true/false to force providing / not providing a menu 4 use_sonata_admin: auto # use true/false to force using / not using sonata admin 5 sonata_admin: 6 sort: false # set to asc|desc to sort children by publication date 7 document_class: Symfony\Cmf\Bundle\SimpleCmsBundle\Document\Page 8 # controller to use to render documents with just custom template 9 generic_controller: cmf_content.controller:indexAction 10 # where in the PHPCR tree to store the pages 11 basepath: /cms/simple 12 routing: 13 content_repository_id: cmf_routing.content_repository 14 controllers_by_class: 15 # ... 16 templates_by_class: 17 # ... 18 multilang: 19 locales: []

If you have the Sonata PHPCR-ODM admin bundle enabled but do NOT want to show the default admin provided by this bundle, you can add the following to your configuration
Listing 27-2

1 cmf_simple_cms: 2 use_sonata_admin: false

Multi-language support
The multi-language-mode is enabled by providing the list of allowed locales in the multilang > locales field. In multi-language-mode the Bundle will automatically use the Symfony\Cmf\Bundle\SimpleCmsBundle\Document\MultilangPage as the document_class unless a different class is configured explicitly. This class will by default prefix all routes with /{_locale}. This behavior can be disabled by setting the second parameter in the constructor of the model to false. Furthermore the routing layer will be configured to use Symfony\Cmf\Bundle\SimpleCmsBundle\Document\MultilangRouteRepository which will ensure that even with the locale prefix the right content node will be found. Furthermore it will automatically add a _locale requirement listing the current available locales for the matched route.
Since SimpleCmsBundle only provides a single tree structure, all nodes will have the same node name for all languages. So a url http://foo.com/en/bar for english content will look like http://foo.com/de/bar for german content. At times it might be most feasible to use integers as the node names and simple append the title of the node in the given locale as an anchor. So for example http://foo.com/de/1#my title and http://foo.com/de/1#mein title. If you need language specific URLs, you want to use the CMF routing bundle and content bundle directly to have a separate route document per language.

PDF brought to you by generated on July 18, 2013

Chapter 27: SimpleCmsBundle | 135

Rendering
You can specify the template to render a SimpleCms page, or use a controller where you then give the page document to the template. A simple example for such a template is:
Listing 27-3

1 {% block content %} 2 <h1>{{ page.title }}</h1> 3 4 <div>{{ page.body|raw }}</div> 5 6 <ul> 7 {% for tag in page.tags %} 8 <li>{{ tag }}</li> 9 {% endfor %} 10 </ul> 11 {% endblock %}

If you have the CreateBundle enabled, you can also output the document with RDFa annotations, allowing you to edit the content as well as the tags in the frontend. The most simple form is the following twig block:
Listing 27-4

1 {% block content %} 2 {% createphp page as="rdf" %} 3 {{ rdf|raw }} 4 {% endcreatephp %} 5 {% endblock %}

If you want to control more detailed what should be shown with RDFa, see chapter CreateBundle.

Extending the Page class


The default Page document Symfony\Cmf\Bundle\SimpleCmsBundle\Document\Page is relatively simple, shipping with a handful of the most common properties for building a typical page: title, body, tags, publish dates etc. If this is not enough for your project you can easily provide your own document by extending the default Page document and explicitly setting the configuration parameter to your own document class:
Listing 27-5

1 # app/config/config.yml 2 cmf_simple_cms: 3 # ... 4 document_class: 5 # ...

Acme\DemoBundle\Document\MySuperPage

Alternatively, the default Page document contains an extras property. This is a key - value store (where value must be string or null) which can be used for small trivial additions, without having to extend the default Page document. For example:
Listing 27-6

1 $page = new Page(); 2 3 $page->setTitle('Hello World!'); 4 $page->setBody('Really interesting stuff...'); 5

PDF brought to you by generated on July 18, 2013

Chapter 27: SimpleCmsBundle | 136

6 7 8 9 10 11 12 13 14

// set extras $extras = array( 'subtext' => 'Add CMS functionality to applications built with the Symfony2 PHP framework.', 'headline-icon' => 'exclamation.png', );
$page->setExtras($extras); $documentManager->persist($page);

These properties can then be accessed in your controller or templates via the getExtras() or getExtra($key) methods.

PDF brought to you by generated on July 18, 2013

Chapter 27: SimpleCmsBundle | 137

Chapter 28

SonataDoctrinePhpcrAdminBundle
The SonataDoctrinePhpcrAdminBundle1 provides integration with the SonataAdminBundle to enable easy creation of admin UIs.

Dependencies
SonataAdminBundle2 TreeBundle3

Configuration
Listing 28-1

1 sonata_doctrine_phpcr_admin: 2 templates: 3 form: 4 5 # Default: 6 - SonataDoctrinePHPCRAdminBundle:Form:form_admin_fields.html.twig 7 filter: 8 9 # Default: 10 - SonataDoctrinePHPCRAdminBundle:Form:filter_admin_fields.html.twig 11 types: 12 list: 13 14 # Prototype 15 name: [] 16 document_tree: 17 # Prototype

1. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle#readme 2. https://github.com/sonata-project/SonataAdminBundle 3. https://github.com/symfony-cmf/TreeBundle#readme

PDF brought to you by generated on July 18, 2013

Chapter 28: SonataDoctrinePhpcrAdminBundle | 138

18 class: # name of the class 19 # class names of valid children, manage tree operations for them and hide 20 other children 21 valid_children: [] image:

PDF brought to you by generated on July 18, 2013

Chapter 28: SonataDoctrinePhpcrAdminBundle | 139

Chapter 29

TreeBrowserBundle
The TreeBrowserBundle1 provides a tree navigation on top of a PHPCR repository. This bundle consists of two parts: Generic Tree Browser with a TreeInterface PHPCR tree implementation and GUI for a PHPCR browser

Dependencies
FOSJsRoutingBundle2 Install jQuery. SonatajQueryBundle3 strongly suggested.

Configuration
The configuration key for this bundle is cmf_tree_browser:
Listing 29-1

1 # app/config/config.yml 2 cmf_tree_browser: 3 session: default

Routing
The bundle will create routes for each tree implementation found. In order to make those routes available you need to include the following in your routing configuration:
Listing 29-2

1. https://github.com/symfony-cmf/TreeBrowserBundle#readme 2. https://github.com/FriendsOfSymfony/FOSJsRoutingBundle 3. https://github.com/sonata-project/SonatajQueryBundle

PDF brought to you by generated on July 18, 2013

Chapter 29: TreeBrowserBundle | 140

1 # app/config/routing.yml 2 cmf_tree: 3 resource: . 4 type: 'cmf_tree'

Usage
You have select.js and init.js which are a wrapper to build a jquery tree. Use them with SelectTree.initTree resp. AdminTree.initTree SelectTree in select.js is a tree to select a node to put its id into a field AdminTree in init.js is a tree to create, move and edit nodes Both have the following options when creating: config.selector: jquery selector where to hook in the js tree config.rootNode: id to the root node of your tree, defaults to "/" config.selected: id of the selected node config.ajax.children_url: Url to the controller that provides the children of a node config.routing_defaults: array for route parameters (such as _locale etc.) config.path.expanded: tree path where the tree should be expanded to at the moment config.path.preloaded: tree path what node should be preloaded for faster user experience

select.js only
config.output: where to write the id of the selected node

init.js only
config.labels: array containing the translations for the labels of the context menu (keys createItem and deleteItem) config.ajax.move_url: Url to the controller for moving a child (i.e. giving it a new parent node) config.ajax.reorder_url: Url to the controller for reordering siblings config.types: array indexed with the node types containing information about valid_children, icons and available routes, used for the creation of context menus and checking during move operations.

Examples
Look at the templates in the Sonata Admin Bundle for examples how to build the tree: init.js4 select.js5 (look for doctrine_phpcr_type_tree_model_widget) In the same bundle the PhpcrOdmTree6 implements the tree interface and gives an example how to implement the methods. Here are some common tips about TreeBrowser utilization:
4. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/blob/master/Resources/views/Tree/tree.html.twig 5. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/blob/master/Resources/views/Form/form_admin_fields.html.twig 6. https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/blob/master/Tree/PhpcrOdmTree.php

PDF brought to you by generated on July 18, 2013

Chapter 29: TreeBrowserBundle | 141

Define Tree Elements


The first step, is to define all the elements allowed in the tree and their children. Have a look at the cmfsandbox configuration7, the section document_tree in sonata_doctrine_phpcr_admin. This configuration is set for all your application trees regardless their type (admin or select).
Listing 29-3

1 sonata_doctrine_phpcr_admin: 2 document_tree_defaults: [locale] 3 document_tree: 4 Doctrine\ODM\PHPCR\Document\Generic: 5 valid_children: 6 - all 7 Symfony\Cmf\Bundle\ContentBundle\Document\MultilangStaticContent: 8 valid_children: 9 - Symfony\Cmf\Bundle\BlockBundle\Document\SimpleBlock 10 - Symfony\Cmf\Bundle\BlockBundle\Document\ContainerBlock 11 - Symfony\Cmf\Bundle\BlockBundle\Document\ReferenceBlock 12 - Symfony\Cmf\Bundle\BlockBundle\Document\ActionBlock 13 Symfony\Cmf\Bundle\BlockBundle\Document\ReferenceBlock: 14 valid_children: [] 15 # ...

How to add an Admin Tree to Your Page


This can be done either in an action template or in a custom block. You have to specify the tree root and the selected item, this allows you to have different type of content in your tree. In this example, we will have the menu elements. For Symfony 2.2 and later:
Listing 29-4

1 {% render(controller('sonata.admin.doctrine_phpcr.tree_controller:treeAction')) with { 2 'root': websiteId ~ "/menu", 3 'selected': menuNodeId, 4 '_locale': app.request.locale 5 } %}

For Symfony 2.1:


Listing 29-5

1 {% render 'sonata.admin.doctrine_phpcr.tree_controller:treeAction' with { 2 'root': websiteId ~ "/menu", 3 'selected': menuNodeId, 4 '_locale': app.request.locale 5 } %}

How to Customize the Tree Behaviour


The TreeBrowserBundle is based on jsTree8. jsTree works with events, dispatched everytime the user does an action. A simple way to customize the tree behavior is to bind your actions to those events.

7. https://github.com/symfony-cmf/cmf-sandbox/blob/master/app/config/config.yml 8. http://www.jstree.com/documentation

PDF brought to you by generated on July 18, 2013

Chapter 29: TreeBrowserBundle | 142

If you have a look at init.js and select.js, you will notice that actions are already bound to some of the tree events. If the default behavior is not what you need, jQuery provide the unbind function to solve the problem. Here is a simple way to remove the context menu from the admin tree (add the controller call around the controller name inside render for Symfony 2.2):
Listing 29-6

{% render 'sonata.admin.doctrine_phpcr.tree_controller:treeAction' with { 'root': websiteId ~ "/menu", 'selected': menuNodeId, '_locale': app.request.locale } %} <script type="text/javascript"> $(document).ready(function() { $('#tree').bind("before.jstree", function (e, data) { if (data.plugin === "contextmenu") { e.stopImmediatePropagation(); return false; } }); }); </script>

By default, the item selection open the edit route of the admin class of the element. This action is bind to the select_node.jstree. If you want to remove it, you just need to call the unbind function on this event:
Listing 29-7

1 <script type="text/javascript"> 2 $(document).ready(function() { 3 $('#tree').unbind('select_node.jstree'); 4 }); 5 </script>

Then you can bind it on another action. For example, if your want to open a custom action:
Listing 29-8

$('#tree').bind("select_node.jstree", function (event, data) { if ((data.rslt.obj.attr("rel") == 'Symfony_Cmf_Bundle_MenuBundle_Document_MenuNode' || data.rslt.obj.attr("rel") == 'Symfony_Cmf_Bundle_MenuBundle_Document_MultilangMenuNode') && data.rslt.obj.attr("id") != '{{ menuNodeId }}' ) { var routing_defaults = {'locale': '{{ locale }}', '_locale': '{{ _locale }}'}; routing_defaults["id"] = data.rslt.obj.attr("url_safe_id"); window.location = Routing.generate('presta_cms_page_edit', routing_defaults); } });

Don't forget to add your custom route to the fos_js_routing.routes_to_expose configuration:


Listing 29-9

1 fos_js_routing: 2 routes_to_expose: 3 - cmf_tree_browser.phpcr_children 4 - cmf_tree_browser.phpcr_move 5 - sonata.admin.doctrine_phpcr.phpcrodm_children 6 - sonata.admin.doctrine_phpcr.phpcrodm_move 7 - presta_cms_page_edit

PDF brought to you by generated on July 18, 2013

Chapter 29: TreeBrowserBundle | 143

Chapter 30

Using a Custom Document Class Mapper with PHPCR-ODM


The default document class mapper of PHPCR-ODM uses the attribute phpcr:class to store and retrieve the document class of a node. When accessing an existing PHPCR repository, you might need different logic to decide on the class. You can extend the DocumentClassMapper or implement DocumentClassMapperInterface from scratch. The important methods are getClassName that needs to find the class name and writeMetadata that needs to make sure the class of a newly stored document can be determined when loading it again. Then you can overwrite the doctrine.odm_configuration service to call setDocumentClassMapper on it. An example from the symfony cmf sandbox1 (magnolia_integration branch):
Listing 30-1

# if you want to overwrite default configuration, otherwise use a # custom name and specify in odm configuration block doctrine.odm_configuration: class: %doctrine_phpcr.odm.configuration.class% calls: - [ setDocumentClassMapper, [@sandbox_magnolia.odm_mapper] ] sandbox_magnolia.odm_mapper: class: "Sandbox\MagnoliaBundle\Document\MagnoliaDocumentClassMapper" arguments: - 'standard-templating-kit:pages/stkSection': 'Sandbox\MagnoliaBundle\Document\Section'

Here you create a mapper that uses a configuration to read node information and map that onto a document class. If you have several repositories, you can use one configuration per repository. See Configuring Multiple Sessions.

1. https://github.com/symfony-cmf/cmf-sandbox/tree/magnolia_integration

PDF brought to you by generated on July 18, 2013

Chapter 30: Using a Custom Document Class Mapper with PHPCR-ODM | 144

Chapter 31

Using a Custom Route Repository with Dynamic Router


The Dynamic Router allows you to customize the Route Provider (i.e. the class responsible for retrieving routes from the database) and, by extension, the Route objects.

Creating the Route Provider


The route provider must implement the RouteProviderInterface. The following class provides a simple solution using an ODM Repository.
Listing 31-1

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

// src/Acme/DemoBundle/Repository/RouteProvider.php namespace Acme\DemoBundle\Repository;


use use use use Doctrine\ODM\PHPCR\DocumentRepository; Symfony\Cmf\Component\Routing\RouteProviderInterface; Symfony\Component\Routing\RouteCollection; Symfony\Component\Routing\Route as SymfonyRoute;

class RouteProvider extends DocumentRepository implements RouteProviderInterface { /** * This method is used to find routes matching the given URL. */ public function findManyByUrl($url) { // for simplicity we retrieve one route $document = $this->findOneBy(array( 'url' => $url, )); $pattern = $document->getUrl(); // e.g. "/this/is/a/url" $collection = new RouteCollection();

PDF brought to you by generated on July 18, 2013

Chapter 31: Using a Custom Route Repository with Dynamic Router | 145

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 }

// create a new Route and set our document as // a default (so that we can retrieve it from the request) $route = new SymfonyRoute($pattern, array( 'document' => $document, )); // add the route to the RouteCollection using // a unique ID as the key. $collection->add('my_route_'.uniqid(), $route);
return $collection; }

/** * This method is used to generate URLs, e.g. {{ path('foobar') }}. */ public function getRouteByName($name, $params = array()) { $document = $this->findOneBy(array( 'name' => $name, ));
if ($route) { $route = new SymfonyRoute($route->getPattern(), array( 'document' => $document, )); } return $route; }

As you may have noticed we return a RouteCollection object - why not return a single Route? The Dynamic Router allows us to return many candidate routes, in other words, routes that might match the incoming URL. This is important to enable the possibility of matching dynamic routes, /page/{page_id}/edit for example. In our example we match the given URL exactly and only ever return a single Route.

Replacing the Default CMF Route Provider


To replace the default RouteProvider, it is necessary to modify your configuration as follows:
Listing 31-2

1 # app/config/config.yml 2 cmf_routing: 3 dynamic: 4 enabled: true 5 route_provider_service_id: acme_demo.provider.endpoint

Where acme_demo.provider.endpoint is the service ID of your route provider. See Creating and configuring services in the container1 for information on creating custom services.

1. http://symfony.com/doc/current/book/service_container.html#creating-configuring-services-in-the-container/

PDF brought to you by generated on July 18, 2013

Chapter 31: Using a Custom Route Repository with Dynamic Router | 146

Chapter 32

Installing the CMF sandbox


This tutorial shows how to install the Symfony CMF Sandbox, a demo platform aimed at showing the tool's basic features running on a demo environment. This can be used to evaluate the platform or to see actual code in action, helping you understand the tool's internals. While it can be used as such, this sandbox does not intend to be a development platform. If you are looking for installation instructions for a development setup, please refer to: Installing the Symfony CMF Standard Edition page for instructions on how to quickly install the CMF (recommended for development) Installing and Configuring the CMF Core for step-by-step installation and configuration details (if you want to know all the details)

Requirements
As Symfony CMF Sandbox is based on Symfony2, you should make sure you meet the Requirements for running Symfony21. Git 1.6+2, Curl3 and PHP Intl are also needed to follow the installation steps listed below. For the PHPCR repository requirements, see PHPCR-ODM requirements

Installation
Apache Jackrabbit
The Symfony CMF Sandbox uses Jackalope with Apache JackRabbit by default. Alternative storage methods can be configured, but this is the most tested, and should be the easiest to setup. You can get the latest Apache Jackrabbit version from the project's official download page4. To start it, use the following command
1. http://symfony.com/doc/current/reference/requirements.html 2. http://git-scm.com/ 3. http://curl.haxx.se/ 4. http://jackrabbit.apache.org/downloads.html

PDF brought to you by generated on July 18, 2013

Chapter 32: Installing the CMF sandbox | 147

Listing 32-1

1 $ java -jar jackrabbit-standalone-*.jar

By default the server is listening on the 8080 port, you can change this by specifying the port on the command line.
Listing 32-2

1 $ java -jar jackrabbit-standalone-*.jar --port 8888

For unix systems, you can get the start-stop script for /etc/init.d here5.

Getting the Sandbox Code


The Symfony CMF Sandbox source code is available on github. To get it use:
Listing 32-3

1 $ git clone git://github.com/symfony-cmf/cmf-sandbox.git

Move into the folder and copy the default configuration files:
Listing 32-4

1 $ cd cmf-sandbox 2 $ cp app/config/parameters.yml.dist app/config/parameters.yml 3 $ cp app/config/phpcr_jackrabbit.yml.dist app/config/phpcr.yml

These two files include the default configuration parameters for the sandbox storage mechanism. You can modify them to better fit your needs
The second configuration file refers to specific jackalope + jackrabbit configuration. There are other files available for different stack setups.

Next, get composer and install and the necessary bundles (this may take a while):
Listing 32-5

1 2 3 4 5

# get composer $ curl -s http://getcomposer.org/installer | php -# install bundles $ php composer.phar install

On Windows you need to run the shell as Administrator or edit the composer.json and change the line "symfony-assets-install": "symlink" to "symfony-assets-install": "". If you fail to do this you might receive:
Listing 32-6

1 [Symfony\Component\Filesystem\Exception\IOException] 2 Unable to create symlink due to error code 1314: 'A required privilege is not held 3 by the client'. Do you have the required Administrator-rights?

Preparing the PHPCR Repository


Now that you have all the code, you need to setup your PHPCR repository. PHPCR organizes data in workspaces and sandbox uses the "default" workspace, which is exists by default in Jackrabbit. If you use
5. https://github.com/sixty-nine/Jackrabbit-startup-script

PDF brought to you by generated on July 18, 2013

Chapter 32: Installing the CMF sandbox | 148

other applications that require Jackrabbit or if you just wish to change the workspace name, you can do so in app/config/phpcr.yml. The following command will create a new workspace named "sandbox" in Jackrabbit. If you decide to use the "default" workspace, you can skip it.
Listing 32-7

1 $ php app/console doctrine:phpcr:workspace:create sandbox

Once your workspace is set up, you need to register the node types6 for phpcr-odm:
Listing 32-8

1 $ php app/console doctrine:phpcr:repository:init

Import the Fixtures


The sandbox provides a set of demo content to show various use cases. They are loaded using the fixture loading concept of PHPCR-ODM.
Listing 32-9

1 $ php app/console -v doctrine:phpcr:fixtures:load

This command loads fixtures from all bundles that provide them in the DataFixtures/PHPCR folder. The sandbox has fixtures in the MainBundle. Note that loading fixtures from non-default locations is possible as well, just not needed in this case.

Accessing your Sandbox


The sandbox should now be accessible on your web server.
Listing 32-10

1 http://localhost/app_dev.php

In order to run the sandbox in production mode you need to generate the doctrine proxies and dump the assetic assets:
Listing 32-11

1 $ php app/console cache:clear --env=prod --no-debug 2 $ php app/console assetic:dump --env=prod --no-debug

Alternative Storage Mechanisms


Symfony CMF and the sandbox are storage agnostic, which means you can change the storage mechanism without having to change your code. The default storage mechanism for the sandbox is Jackalope + Apache Jackrabbit, as it's the most tested and stable setup. However, other alternatives are available.

Jackalope + Doctrine DBAL


By default, when using Doctrine DBAL, data is stored using a Sqlite7 database. Refer to the project's page for installation instructions. If you wish to use other database systems, change the configuration parameters in app/config/parameters.yml. Refer to Symfony's page on Doctrine DBAL configuration8 or Doctrine's documentation9 for more information.

6. https://github.com/doctrine/phpcr-odm/wiki/Custom-node-type-phpcr%3Amanaged 7. http://www.sqlite.org/

PDF brought to you by generated on July 18, 2013

Chapter 32: Installing the CMF sandbox | 149

Move into the sandbox folder and copy the default configuration file for Doctrine DBAL setup:
Listing 32-12

1 $ cd cmf-sandbox 2 $ cp app/config/phpcr_doctrine_dbal.yml.dist app/config/phpcr.yml

Next, you need to install the actual Doctrine DBAL bundle required by jackalope:
Listing 32-13

1 $ php composer.phar require jackalope/jackalope-doctrine-dbal:dev-master

And create and init your database:


Listing 32-14

1 $ php app/console doctrine:database:create 2 $ php app/console doctrine:phpcr:init:dbal

After this, your should follow the steps in Preparing the PHPCR repository.

Doctrine caching
Optionally, to improve performance and enable the meta data, you can install LiipDoctrineCacheBundle by typing the following command:
Listing 32-15

1 $ php composer.phar require liip/doctrine-cache-bundle:dev-master

And adding the following entry to your app/AppKernel.php:


Listing 32-16

1 2 3 4 5 6 7 8 9 10 11

// app/AppKernel.php // ... public function registerBundles() { $bundles = array( // ... new Liip\DoctrineCacheBundle\LiipDoctrineCacheBundle(), // ... ); }

Finally, uncomment the caches settings in the phpcr.yml as well as the liip_doctrine_cache settings in config.yml.
Listing 32-17

1 # app/config/phpcr.yml 2 caches: 3 meta: liip_doctrine_cache.ns.meta 4 nodes: liip_doctrine_cache.ns.nodes

Listing 32-18

1 2 3 4 5 6 7

# app/config/config.yml # ... # jackalope doctrine caching liip_doctrine_cache: namespaces:

8. http://symfony.com/doc/current/reference/configuration/doctrine.html#doctrine-dbal-configuration 9. http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html

PDF brought to you by generated on July 18, 2013

Chapter 32: Installing the CMF sandbox | 150

8 9 10 11

meta: type: file_system nodes: type: file_system

Midgard2 PHPCR Provider


If you want to run the CMF sandbox with the Midgard2 PHPCR10 provider instead of Jackrabbit, you need to install the midgard2 PHP extension. On current Debian/Ubuntu systems, this is simply done with:
Listing 32-19

1 $ sudo apt-get install php5-midgard2

On OS X you can install it using either Homebrew11 with:


Listing 32-20

1 $ brew install midgard2-php

or MacPorts12 with:
Listing 32-21

1 $ sudo port install php5-midgard2

You also need to download the midgard_tree_node.xml13 and midgard_namespace_registery.xml14 schema files and place them into <your-midgard2-folder>/schema (defaults to "/usr/share/midgard2/ schema") To have the Midgard2 PHPCR implementation installed run the following additional command:
Listing 32-22

1 $ php composer.phar require midgard/phpcr:dev-master

Finally, switch to one of the Midgard2 configuration file:


Listing 32-23

1 $ cp app/config/phpcr_midgard_mysql.yml.dist app/config/phpcr.yml

or:
Listing 32-24

1 $ cp app/config/phpcr_midgard_sqlite.yml.dist app/config/phpcr.yml

After this, your should follow the steps in Preparing the PHPCR repository to continue the installation process.

10. http://midgard-project.org/phpcr/ 11. http://mxcl.github.com/homebrew/ 12. http://www.macports.org/ 13. https://raw.github.com/midgardproject/phpcr-midgard2/master/data/share/schema/midgard_tree_node.xml 14. https://github.com/midgardproject/phpcr-midgard2/raw/master/data/share/schema/midgard_namespace_registry.xml

PDF brought to you by generated on July 18, 2013

Chapter 32: Installing the CMF sandbox | 151

Chapter 33

Routing
The Symfony CMF Routing component1 library extends the Symfony2 core routing component. Even though it has Symfony in its name, it does not need the full Symfony2 framework and can be used in standalone projects. For integration with Symfony we provide RoutingBundle. At the core of the Symfony CMF Routing component is the ChainRouter, that is used instead of the Symfony2's default routing system. The ChainRouter can chain several RouterInterface implementations, one after the other, to determine what should handle each request. The default Symfony2 router can be added to this chain, so the standard routing mechanism can still be used. Additionally, this component is meant to provide useful implementations of the routing interfaces. Currently, it provides the DynamicRouter, which uses a RequestMatcherInterface to dynamically load Routes, and can apply RouteEnhancerInterface strategies in order to manipulate them. The provided NestedMatcher can dynamically retrieve Symfony2 Route2 objects from a RouteProviderInterface. This interfaces abstracts a collection of Routes, that can be stored in a database, like Doctrine PHPCRODM or Doctrine ORM. The DynamicRouter also uses a UrlGenerator instance to generate Routes and an implementation is provided under ProviderBasedGenerator that can generate routes loaded from a RouteProviderInterface instance, and the ContentAwareGenerator on top of it to determine the route object from a content object.
To use this component outside of the Symfony2 framework context, have a look at the core Symfony2 Routing3 to get a fundamental understanding of the component. CMF Routing just extends the basic behaviour.

Dependencies
This component uses Composer4. It needs the Symfony2 Routing5 component and the Symfony2 HttpKernel6 (for the logger interface and cache warm-up interface).
1. https://github.com/symfony/symfony-docs/issues?milestone=1&state=open 2. http://api.symfony.com/master/Symfony/Component/Routing/Route.html 3. http://symfony.com/doc/current/components/routing/introduction.html 4. http://getcomposer.org 5. http://symfony.com/doc/current/components/routing/introduction.html

PDF brought to you by generated on July 18, 2013

Chapter 33: Routing | 152

For the DynamicRouter you will need something to implement the RouteProviderInterface with. We suggest using Doctrine as this provides an easy way to map classes into a database.

ChainRouter
At the core of Symfony CMF's Routing component sits the ChainRouter. It's used as a replacement for Symfony2's default routing system, and is responsible for determining the parameters for each request. Typically you need to determine which Controller will handle this request - in the full stack Symfony2 framework, this is identified by the _controller field of the parameters. The ChainRouter works by accepting a set of prioritized routing strategies, RouterInterface7 implementations, commonly referred to as "Routers". When handling an incoming request, the ChainRouter iterates over the configured routers, by their configured priority, until one of them is able to match()8 the request and provide the request parameters.

Routers
The ChainRouter is incapable of, by itself, making any actual routing decisions. Its sole responsibility is managing the given set of Routers, which are responsible for matching a request and determining its parameters. You can easily create your own routers by implementing RouterInterface9 but Symfony CMF already includes a powerful route matching system that you can extend to your needs.
If you are using this as part of a full Symfony CMF project, please refer to RoutingBundle for instructions on how to add Routers to the ChainRouter. Otherwise, use the ChainRouter's add method to configure new routers.

Symfony2 Default Router


The Symfony2 routing mechanism is itself a RouterInterface implementation, which means you can use it as a Router in the ChainRouter. This allows you to use the default routing declaration system.

Dynamic Router
The Symfony2 default Router was developed to handle static Route definitions, as they are traditionally declared in configuration files, prior to execution. This makes it a poor choice to handle dynamically defined routes, and to handle those situations, this bundle comes with the DynamicRouter. It is capable of handling Routes from more dynamic data sources, like database storage, and modify the resulting parameters using a set of enhancers that can be easily configured, greatly extending Symfony2's default functionality.

Matcher
The DynamicRouter uses a RequestMatcherInterface or UrlMatcherInterface instance to match the received Request or URL, respectively, to a parameters array. The actual matching logic depends on the underlying implementation you choose. You can easily use you own matching strategy by passing it to

6. http://symfony.com/doc/current/components/http_kernel/introduction.html 7. http://api.symfony.com/master/Symfony/Component/Routing/RouterInterface.html 8. http://api.symfony.com/master/Symfony/Component/Routing/RouterInterface.html#match() 9. http://api.symfony.com/master/Symfony/Component/Routing/RouterInterface.html

PDF brought to you by generated on July 18, 2013

Chapter 33: Routing | 153

the DynamicRouter constructor. As part of this bundle, a NestedMatcher is already provided which you can use straight away, or as reference for your own implementation. Its other feature are the RouteEnhancerInterface strategies used to infer routing parameters from the information provided by the match (see below).

NestedMatcher
The provided RequestMatcherInterface implementation is NestedMatcher. It is suitable for use with DynamicRouter, and it uses a multiple step matching process to determine the resulting routing parameters from a given Request10. It uses a RouteProviderInterface implementation, which is capable of loading candidate Route11 objects for a Request dynamically from a data source. Although it can be used in other ways, the RouteProviderInterface's main goal is to be easily implemented on top of Doctrine PHPCR ODM or a relational database, effectively allowing you to store and manage routes dynamically from database. The NestedMatcher uses a 3-step matching process to determine which Route to use when handling the current Request: Ask the RouteProviderInterface for the collection of Route instances potentially matching the Request Apply all RouteFilterInterface to filter down this collection Let the FinalMatcherInterface instance decide on the best match among the remaining Route instances and transform it into the parameter array.
RouteProviderInterface

Based on the Request, the NestedMatcher will retrieve an ordered collection of Route objects from the RouteProviderInterface. The idea of this provider is to provide all routes that could potentially match, but not to do any elaborate matching operations yet - this is the job of the later steps. The underlying implementation of the RouteProviderInterface is not in the scope of this bundle. Please refer to the interface declaration for more information. For a functional example, see RoutingBundle.
RouteFilterInterface

The NestedMatcher can apply user provided RouteFilterInterface implementations to reduce the provided Route objects, e.g. for doing content negotiation. It is the responsibility of each filter to throw the ResourceNotFoundException if no more routes are left in the collection.
FinalMatcherInterface

The FinalMatcherInterface implementation has to determine exactly one Route as the best match or throw an exception if no adequate match could be found. The default implementation uses the UrlMatcher12 of the Symfony Routing Component.

Route Enhancers
Optionally, and following the matching process, a set of RouteEnhancerInterface instances can be applied by the DynamicRouter. The aim of these is to allow you to manipulate the parameters from the matched route. They can be used, for example, to dynamically assign a controller or template to a Route or to "upcast" a request parameter to an object. Some simple Enhancers are already packed with the bundle, documentation can be found inside each class file.

10. http://api.symfony.com/master/Symfony/Component/HttpFoundation/Request.html 11. http://api.symfony.com/master/Symfony/Component/Routing/Route.html 12. http://api.symfony.com/master/Symfony/Component/Routing/Matcher/UrlMatcher.html

PDF brought to you by generated on July 18, 2013

Chapter 33: Routing | 154

Linking a Route with a Content


Depending on your application's logic, a requested url may have an associated content from the database. Those Routes should implement the RouteObjectInterface, and can optionally return a model instance. If you configure the RouteContentEnhancer, it will included that content in the match array, with the _content key. Notice that a Route can implement the above mentioned interface but still not to return any model instance, in which case no associated object will be returned. Furthermore, routes that implement this interface can also provide a custom Route name. The key returned by getRouteKey will be used as route name instead of the Symfony core compatible route name and can contain any characters. This allows you, for example, to set a path as the route name. Both UrlMatchers provided with the NestedMatcher replace the _route key with the route instance and put the provided name into _route_name. All routes still need to extend the base class Symfony\Component\Routing\Route.

Redirections
You can build redirections by implementing the RedirectRouteInterface. It can redirect either to an absolute URI, to a named Route that can be generated by any Router in the chain or to another Route object provided by the Route. Notice that the actual redirection logic is not handled by the bundle. You should implement your own logic to handle the redirection. For an example on implementing that redirection under the full Symfony2 stack, refer to RoutingBundle.

Generating URLs
Apart from matching an incoming request to a set of parameters, a Router is also responsible for generating an URL from a Route and its parameters. The ChainRouter iterates over its known routers until one of them is able to generate a matching URL. Apart from using RequestMatcherInterface or UrlMatcherInterface to match a Request/URL to its corresponding parameters, the DynamicRouter also uses an UrlGeneratorInterface instance, which allows it to generate an URL from a Route. The included ProviderBasedGenerator extends Symfony2's default UrlGenerator13 (which, in turn, implements UrlGeneratorInterface) and - if $name is not already a Route object - loads the route from the RouteProviderInterface. It then lets the core logic generate the URL from that Route. The bundle also include the ContentAwareGenerator, which extends the ProviderBasedGenerator to check if $name is an object implementing RouteAwareInterface and, if so, gets the Route from the content. Using the ContentAwareGenerator, you can generate urls for your content in three ways: Either pass a Route object as $name Or pass a RouteAwareInterface object that is your content as $name Or provide an implementation of ContentRepositoryInterface and pass the id of the content object as parameter content_id and null as $name.

ContentAwareGenerator and locales


You can use the _locale default value in a Route to create one Route per locale, all referencing the same multilingual content instance. The ContentAwareGenerator respects the _locale when generating routes from content instances. When resolving the route, the _locale gets into the request and is picked up by the Symfony2 locale system.

13. http://api.symfony.com/master/Symfony/Component/routing/Generator/UrlGenerator.html

PDF brought to you by generated on July 18, 2013

Chapter 33: Routing | 155

Under PHPCR-ODM, Routes should never be translatable documents, as one Route document represents one single url, and serving several translations under the same url is not recommended. If you need translated URLs, make the locale part of the route name.

Customization
The Routing bundles allows for several customization options, depending on your specific needs: You can implement your own RouteProvider to load routes from a different source Your Route parameters can be easily manipulated using the existing Enhancers You can also add your own Enhancers to the DynamicRouter You can add RouteFilterInterface instances to the NestedMatcher The DynamicRouter or its components can be extended to allow modifications You can implement your own Routers and add them to the ChainRouter
If you feel like your specific Enhancer or Router can be useful to others, get in touch with us and we'll try to include it in the bundle itself

Symfony2 integration
Like mentioned before, this bundle was designed to only require certain parts of Symfony2. However, if you wish to use it as part of your Symfony CMF project, an integration bundle is also available. We strongly recommend that you take a look at RoutingBundle. For a starter's guide to the Routing bundle and its integration with Symfony2, refer to Routing We strongly recommend reading Symfony2's Routing14 component documentation page, as it's the base of this bundle's implementation.

Authors
Filippo De Santis (p16) Henrik Bjornskov (henrikbjorn) Claudio Beatrice (omissis) Lukas Kahwe Smith (lsmith77) David Buchmann (dbu) Larry Garfield (Crell) And others15

The original code for the chain router was contributed by Magnus Nordlander.

14. http://symfony.com/doc/current/components/routing/introduction.html 15. https://github.com/symfony/symfony-docs/issues?milestone=1&state=open

PDF brought to you by generated on July 18, 2013

Chapter 33: Routing | 156

Chapter 34

Testing
The Testing component is an internal tool for testing Symfony CMF bundles. It provides a way to easily bootstrap a consistent functional test environment.

Configuration
composer
Add the folowing dependency to the require-dev section of composer.json:
Listing 34-1

1 "require-dev": { 2 "symfony-cmf/testing": "1.0.*" 3 },

The Testing component does not automatically include the SonataAdminBundle. You will need to manually add this dependency if required.

phpunit
The following file should be placed in the root directory of your bundle and named phpunit.xml.dist:
Listing 34-2

1 <?xml version="1.0" encoding="UTF-8"?> 2 <phpunit 3 colors="true" 4 bootstrap="vendor/symfony-cmf/testing/bootstrap/bootstrap.php" 5 > 6 7 <testsuites> 8 <testsuite name="Symfony <your bundle>Bundle Test Suite"> 9 <directory>./Tests</directory>

PDF brought to you by generated on July 18, 2013

Chapter 34: Testing | 157

10 </testsuite> 11 </testsuites> 12 13 <filter> 14 <whitelist addUncoveredFilesFromWhitelist="true"> 15 <directory>.</directory> 16 <exclude> 17 <file>*Bundle.php</file> 18 <directory>Resources/</directory> 19 <directory>Admin/</directory> 20 <directory>Tests/</directory> 21 <directory>vendor/</directory> 22 </exclude> 23 </whitelist> 24 </filter> 25 26 <php> 27 <server name="KERNEL_DIR" value="Tests/Resources/app" /> 28 </php> 29 30 </phpunit>

AppKernel
The AppKernel should be placed in the ./Tests/Resources/app folder. Below is the minimal AppKernel.php:
Listing 34-3

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

<?php use Symfony\Cmf\Component\Testing\HttpKernel\TestKernel; use Symfony\Component\Config\Loader\LoaderInterface; class AppKernel extends TestKernel { public function configure() { $this->requireBundleSets(array( 'default', )); $this->addBundles(array( new \Symfony\Cmf\Bundle\MyBundle\CmfMyBundle(), )); } public function registerContainerConfiguration(LoaderInterface $loader) { $loader->load(__DIR__.'/config/config.php'); } }

Use $this->requireBundleSets('bundle_set_name') to include pre-configured sets of bundles: default: Symfony's FrameworkBundle, TwigBundle and MonologBundle; phpcr_odm: Doctrines DoctrineBundle and DoctrinePHPCRBundle; sonata_admin: Sonata AdminBundle, BlockBundle SonataDoctrinePHPCRAdminBundle.
PDF brought to you by generated on July 18, 2013

and

Chapter 34: Testing | 158

For any other bundle requirements simply use $this->addBundles(array()) as in the example above.

git
Place the following .gitignore file in your root directory:
Listing 34-4

1 2 3 4

Tests/Resources/app/cache Tests/Resources/app/logs composer.lock vendor

travis
The following file should be named .travis.yml (note the leading ".") and placed in the root directory of your bundle:
Listing 34-5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

language: php php: - 5.3 - 5.4 - 5.5 env: - SYMFONY_VERSION=2.2.* - SYMFONY_VERSION=2.3.* before_script: - composer require symfony/framework-bundle:${SYMFONY_VERSION} - vendor/symfony-cmf/testing/bin/travis/phpcr_odm_doctrine_dbal.sh script: phpunit --coverage-text notifications: irc: "irc.freenode.org#symfony-cmf" email: "symfony-cmf-devs@googlegroups.com"

Implementing the Component


You should try and build a working application for testing your bundle. The application can be accessed using the server command detailed in this document.

Test Types
Unit - The scope of a unit test should be limited testing a single class instance. All other dependencies should be mocked; Functional - Functional tests will test a single service as retrieved from the dependency injection container; Web - Web test cases are the most holistic tests. They use the browser kit to make web requests on the kernel, testing the whole stack.

PDF brought to you by generated on July 18, 2013

Chapter 34: Testing | 159

Test File Organization


Test files and tests should be organized as follows:
Listing 34-6

1 ./Tests/ 2 ./Unit 3 ./Full/Namespace/<test>Test.php 4 ./Document/BlogTest.php 5 ./Document/PostTest.php 6 [...] 7 ./Functional 8 ./MyService/SomeServiceTest.php 9 [...] 10 ./WebTest 11 ./Admin/SomeAdminTest.php 12 ./Controller/MyControllerTest.php 13 ./Resources 14 ./app 15 ./AppKernel.php 16 ./config/ 17 ./config.php

Custom Documents
The testing component will automatically include PHPCR-ODM documents that are placed in Tests/ Resources/Document.

Configuration
The testing component includes some pre-defined configurations to get things going with a minimum of effort and repetition. To implement the default configurations create the following PHP file:
Listing 34-7

1 2 3 4 5

<?php // Tests/Resources/app/config/config.php $loader->import(CMF_TEST_CONFIG_DIR.'/default.php'); $loader->import(__DIR__.'/mybundleconfig.yml');

Here you include the testing components default configuration, which will get everything up-andrunning. You can then optionally import configurations specific to your bundle. The available default configurations are as follows, and correspond to the bundle sets above: default.php: framework, doctrine, security; sonata_admin.php: sonata_admin, sonata_block; phpcr-odm.php: doctrine_phpcr. Note that each must be prefixed with the CMF_TEST_CONFIG_DIR constant.

Routing Configuration
You must include a routing.php file in the same directory as the configuration above:
Listing 34-8

1 <?php 2

PDF brought to you by generated on July 18, 2013

Chapter 34: Testing | 160

3 4 5 6 7 8 9 10 11 12 13

use Symfony\Component\Routing\RouteCollection; $collection = new RouteCollection(); $collection->addCollection( $loader->import(CMF_TEST_CONFIG_DIR.'/routing/sonata_routing.yml') ); $collection->addCollection( $loader->import(__DIR__.'/routing/my_test_routing.yml') ); return $collection;

The following default routing configurations are available: sonata_routing.yml: sonata admin and dashboard. The above files must be prefixed with CMF_TEST_CONFIG_DIR.'routing' as in the example above.

The Console
The console for your test application can be accessed as follows:
Listing 34-9

1 $ php vendor/symfony-cmf/testing/bin/console

Test Web Server


The testing component provides a wrapper for the Symfony server:run command.
Listing 34-10

1 $ php vendor/symfony-cmf/testing/bin/server

Which basically does the following:


Listing 34-11

1 $ php vendor/symfony-cmf/testing/bin/console server:run \ 2 --router=vendor/symfony-cmf/testing/resources/web/router.php \ 3 --docroot=vendor/symfony-cmf/testing/resources/web

You can then access your test application in your browser at http://localhost:8000. Publish assets in the directory named above using the testing console as follows:
Listing 34-12

1 $ php vendor/symfony-cmf/testing/bin/cosole assets:install \ 2 vendor/symfony-cmf/testing/resources/web

Initializing the Test Environment


Before running your (functional) tests you will need to initialize the test environment (i.e. the database). You could do this manually, but it is easier to do this the same way that travis will do it, as follows:
Listing 34-13

1 $ ./vendor/symfony-cmf/testing/bin/travis/phpcr_odm_doctrine_dbal.sh

PDF brought to you by generated on July 18, 2013

Chapter 34: Testing | 161

Chapter 35

Functional and Web Testing


In general your functional tests should extend Symfony\Cmf\Component\Testing\Functional\BaseTestCase. This class will provide you with some helpers to make testing easier.

PHPCR-ODM
Accessing the Document Manager
Access as:
Listing 35-14

1 2 3 4 5 6 7 8 9 10 11 12

<?php $manager = $this->db('PHPCR'); $documentManager = $this->db('PHPCR')->getOm();

// create a test node /test $this->db('PHPCR')->createTestNode(); // load fixtures $this->db('PHPCR')->loadFixtures(array( // ... fixture classes here ));

Support Files
The testing component includes some basic documents which will automatically be mapped by PHPCRODM: Symfony\Cmf\Testing\Document\Content: Minimal referenceable content document.

PDF brought to you by generated on July 18, 2013

Chapter 35: Functional and Web Testing | 162

Chapter 36

Contributing
The Symfony2 CMF team follows all the rules and guidelines of the core Symfony2 development process1. When creating Pull Requests, please follow the Symfony Submitting a Patch2 guidlines.

Resources / Links
GitHub3 Website4 Wiki5 Issue Tracker6 IRC channel7 Users mailing list8 Devs mailing list9

1. http://symfony.com/doc/current/contributing/index.html 2. http://symfony.com/doc/current/contributing/code/patches.html 3. https://github.com/symfony-cmf 4. http://cmf.symfony.com/ 5. https://github.com/symfony-cmf/symfony-cmf/wiki 6. http://github.com/symfony-cmf/symfony-cmf/issues 7. #cmf-contributing-irc:--freenode--symfony-cmf 8. http://groups.google.com/group/symfony-cmf-users 9. http://groups.google.com/group/symfony-cmf-devs

PDF brought to you by generated on July 18, 2013

Chapter 36: Contributing | 163

Chapter 37

The Symfony CMF Release Process


This document explains the release process for the Symfony Content Management Framework. See the core documentation for the core Symfony release process1. Symfony CMF manages its releases through a time-based model; a new Symfony CMF release comes out every six months. We want to synchronize with the core Symfony release dates and release a version about one month after Symfony. This should lead to releases in June and December. This release cycle is for the ``symfony-cmf/symfony-cmf`` repository and the symfony-cmf Standard Edition. The individual CMF bundles and components may release minor versions more often, if needed. symfony-cmf/symfony-cmf will always point to a working combination and only integrate newer minor versions when a minor release of the CMF is scheduled. Point releases (i.e. 1.0.1) are used to quickly provide important fixes. New features are never added in the point releases, but only in minor releases. With the release of 1.0, we will create a branch 1.0 to maintain such fixes, and master becomes aliased to 1.1.x-dev. The CMF Standard Edition and symfony-cmf will get point releases whenever one of the included bundles does a point release.
The CMF is quite new. As with Symfony 2.0, we don't want to promise BC at all cost yet. If reasonably doable, we will keep the code BC or use deprecations, but there might be exceptions where BC is too cumbersome. The UPGRADE.md document will help you in those cases.

Development
The six-months period is divided into two phases: Development: Four months to add new features and to enhance existing ones; Stabilisation: Two months to fix bugs and prepare the release. During the development phase, any new feature can be reverted if it won't be finished in time or if it won't be stable enough to be included in the current final release.

1. http://symfony.com/doc/current/contributing/community/releases.html

PDF brought to you by generated on July 18, 2013

Chapter 37: The Symfony CMF Release Process | 164

Maintenance
The CMF is a community effort and as such, no maintenance can be guaranteed. If you need maintenance contracts, please get in contact with the lead developers. They work at internet agencies and will be able to offer support and maintenance contracts.

Backward Compatibility
For the next releases, we will maintain BC if possible. If something needs to be broken, the UPGRADE.md file will help you to update the project. We will switch to a BC-at-all-cost model later, once the CMF has stabilized and matured.
The work on Symfony CMF 2.0 will start whenever enough major features breaking backward compatibility are waiting on the todo-list.

Deprecations
When a feature implementation cannot be replaced with a better one without breaking backward compatibility, there is still the possibility to deprecate the old implementation and add a new preferred one along side. Read the conventions document to learn more about how deprecations are handled in Symfony.

Rationale
This release process was adopted to give more predictability and transparency. It is heavily inspired by the core Symfony release process2.

2. http://symfony.com/doc/current/contributing/community/releases.html

PDF brought to you by generated on July 18, 2013

Chapter 37: The Symfony CMF Release Process | 165

Chapter 38

Licensing
The Symfony2 CMF aims to provide liberal open source licenses for its entire stack.

Code
The code stack is covered by the Apache license1 for Jackalope and PHPCR, the rest of the stack, notably the Symfony2 code, PHPCR-ODM, create.js and Hallo.js are MIT licensed2. Please refer to the relevant LICENSE files in the given source packages.

Documentation
The Symfony CMF documentation is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License3. You are free: to Share to copy, distribute and transmit the work; to Remix to adapt the work. Under the following conditions: Attribution You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work); Share Alike If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one. With the understanding that: Waiver Any of the above conditions can be waived if you get permission from the copyright holder;

1. http://en.wikipedia.org/wiki/Apache_license 2. http://en.wikipedia.org/wiki/MIT_License 3. http://creativecommons.org/licenses/by-sa/3.0/

PDF brought to you by generated on July 18, 2013

Chapter 38: Licensing | 166

Public Domain Where the work or any of its elements is in the public domain under applicable law, that status is in no way affected by the license; Other Rights In no way are any of the following rights affected by the license: Your fair dealing or fair use rights, or other applicable copyright exceptions and limitations; The author's moral rights; Rights other persons may have either in the work itself or in how the work is used, such as publicity or privacy rights. Notice For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page. This is a human-readable summary of the Legal Code (the full license)4.

4. http://creativecommons.org/licenses/by-sa/3.0/legalcode

PDF brought to you by generated on July 18, 2013

Chapter 38: Licensing | 167

Das könnte Ihnen auch gefallen