Symfony2 + PHPCR + Doctrine2 + Jackalope recipe

Lately I’ve followed some developments in the Symfony2 corner of the PHP community with great interest. One of the most enticing developments is the usage of a Content Repository as a backend for your CMS. There is some work being done on the Symfony CMF, combining Symfony2, Doctrine2, PHPCR and Jackalope into a set of tools for building CMS’es based on a Content Repository backend. I didn’t get anything of the CMF to run yet, so I decided to dive in to tying these separate techniques together myself, and get a little proof-of-concept working. Here’s the code, and here’s the recipe:

I do not intend to convince you to use a content repository. I think the matters speak for themselves. If you know all the limitations classical setups using relational databases have, you’ll probably see a future in NoSQL databases, as they are affectionately called. MongoDB and CouchDB never convinced me the way the JCR specification did. Google the specs and read them yourself and know what I mean.

1. Get a CR backend running

I chose Apache Jackrabbit, a Java Content Repository (JCR), to do this. As pointed out on the Jackalope website, you’ll need a patched version of Jackrabbit to get things compatible with PHPCR. Actually, it is just version 2.2 with a backport patch of the 2.3 branch applied to it 1, so don’t fear using it. I’m told it is pretty stable.

  1. Get the patched Jackrabbit here.
  2. Run the the server

    java -jar jackrabbit-standalone-*.jar

This will run jackrabbit at port 8080. Visit your jackrabbit front end at http://localhost:8080/. You’ll be asked a username and password, but you can leave them blank. By default, there is no access control. If you see a page about jackrabbit, it works.

2. Install Symfony

I’m assuming you know how to do this. If you don’t, follow the instructions at symfony.com. The short version is:

  1. Download Symfony 2.0 latest version
  2. Extract the archive in your document root, say /var/www/
  3. Visit /Symfony/web/config.php and follow the instructions
  4. Visit /Symfony/web/app_dev.php to determine if the installation worked

3. Install PHPCR, Jackalope, DoctrinePHPCR ODM and the DoctrinePHPCRBundle

Go to your vendor dir and clone the PHPCR libraries from github:

cd /var/www/Symfony/vendor
git clone git://github.com/phpcr/phpcr.git
git clone git://github.com/jackalope/jackalope.git
git clone git://github.com/doctrine/phpcd-odm.git
cd bundles/Symfony/Bundle
git clone git://github.com/symfony-cmf/DoctrinePHPCRBundle.git

4. Configure the autoloader

Open app/autoload.php and configure the namespaces for the downloaded libraries:

<?php
/* ... */
$loader->registerNamespaces(array(
    /* ... */
    'Jackalope'             => __DIR__.'/../vendor/jackalope/src',
    'PHPCR'                 => __DIR__.'/../vendor/phpcr/src',
    'Doctrine\\ODM\\PHPCR'  => __DIR__.'/../vendor/phpcr-odm/lib/',
    /* ... */
));

5. Add the DoctrinePHPCRBundle to your AppKernel

Open app/AppKernel.php and add the bundle to the registerBundles() call:

<?php
/* ... */
public function registerBundles()
{
    $bundles = array(
        /* ... */
        new Symfony\Bundle\DoctrinePHPCRBundle\DoctrinePHPCRBundle()
    );
    /* ... */
}

6. Configure the ODM default service

Add the following section to your app/config/config.yml file:

doctrine_phpcr:
    session:
        backend:
            url: http://localhost:8080/server/
        workspace: default
        username: ''
        password: ''
    odm:
        auto_mapping: true

7. Set up the Doctrine system types

To have Jackrabbit understand some type internals Doctrine uses for document mapping, we’ll need to make the content repository aware of these types. There is a console command for that, to make things easy:

app/console doctrine:phpcr:register-system-node-types

If you skip this step, persisting documents with a Doctrine DocumentManager will fail with a Jackrabbit error message complaining about phpcr being an unknown namespace.

8. Set up a PagesBundle

You should, of course, change the class and namespace names to whatever you wish. Here’s what I used:

In src/Melp, I created a PagesBundle. This bundle will hold my routes for editing, creating, deleting and showing pages from the content repository. It will also hold the Document model classes I will be using for mapping documents to objects:

cd /var/www/Symfony/src
mkdir -p Melp/PagesBundle/{Controller,Document,Resources{,/views}}

This creates the bundle’s directory structure. There is also a console command for it, which can scaffold the directory structure and some classes for you:

cd /var/www/Symfony
app/console generate:bundle

The MelpPagesBundle class will be in /var/www/Symfony/src/Melp/PagesBundle/MelpPagesBundle.php, and contains the following code:

<?php
namespace Melp\PagesBundle;
 
use Symfony\Component\HttpKernel\Bundle\Bundle;
 
class MelpPagesBundle extends Bundle {}

Add the bundle instance to the AppKernel class’s bundle initialization routine:

<?php
/* ... */
public function registerBundles()
{
    $bundles = array(
        /* ... */
        new Melp\Bundle\PagesBundle\MelpPagesBundle()
    );
    /* ... */
}

9. Set up the controller, routes and view scripts

I’ll use Annotations for the controller set up. I was a bit reluctant at first to use annotations for such configuration, but I find it much easier and clear to use annotations than the other configuration options. The only thing we’ll need is to let the router configuration know the controller exists. We’ll do that in app/config/routing.yml:

_pages:
    resource:  "@MelpPagesBundle/Controller/PageController.php"
    type: annotation

I will also use Twig view scripts. You can of course use PHP view scripts or whatever you prefer, but Twig has had my heart for some time already. The @Template annotation will tell Symfony to render the view resources using the twig template engine as is configured in the default Symfony2 distribution you downloaded. 2

The controller class will then look like this:

<?php
namespace Melp\PagesBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 
class PageController extends Controller
{
    /**
     * @Route("/")
     */
    function indexAction()
    {
        return $this->redirect($this->generateUrl('view', array('path' => 'home')));
    }
 
 
    /**
     * @Route("/{path}", requirements={"path" = "(?!(edit|delete|create)).+"}, name="view")
     * @Template
     */
    function viewAction($path)
    {
    }
 
 
    /**
     * @Route("/edit/{path}", requirements={"path" = ".*"}, name="edit")
     * @Template
     */
    function editAction($path)
    {
    }
 
    /**
     * @Route("/create/{path}", requirements={"path" = ".*"}, name="create")
     */
    function createAction($path)
    {
    }
 
 
    /**
     * @Route("/delete/{path}", requirements={"path" = ".*"}, name="delete")
     */
    function deleteAction($path)
    {
    }
}

Since I want to have the routes reflect the path in the content repository, I use a wildcard pattern for the paths, so they may contain slashes. You can use PCRE in your routes, which I used to exclude the /edit, /create and /delete routes from the default viewAction() to avoid conflicts.

These actions will implement CRUD actions for nodes inside the content repository.

10. Create the document model class

To have Doctrine manage my model class, I will need to attach Doctrine annotations to it, so Doctrine will know how to read and write model data from and to the content repository backend. I will use a simple Page class, with title and content properties, wich we will later implement the CRUD for, as layed out in the Controller above.

In src/Melp/PagesBundle/Document, create the model reflecting the ‘Page’ data structure we’ll be using to edit and show pages 3.

<?php
namespace Melp\PagesBundle\Document;
 
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;
 
/**
 * @PHPCRODM\Document(alias="page")
 */
class Page
{
    /** @PHPCRODM\Id */
    public $path;
 
    /** @PHPCRODM\String */
    public $title;
 
    /** @PHPCRODM\String */
    public $content;
}

11. Create a Pages facade and register it as a service

I created a facade for controlling how pages are persisted and added some logic to that for placing pages in a configured root:

services:
    pages:
        class: Melp\PagesBundle\Service\PagesFacade
        arguments:
            - @doctrine_phpcr.odm.default_document_manager
            - 'Melp\PagesBundle\Document\Page'
            - "/pages/"
            - "home"

The facade exposes the following methods:

public function __construct(\Doctrine\ODM\PHPCR\DocumentManager $dm, $documentClass, $root, $default)
{
}


function createDefault($title = "Homepage")
{
}


function removePath($path)
{
}


function findPath($path)
{
}


function remove($node)
{
}


function persist($node)
{
}

This saves some clutter in the controller code. The Facade dispatches to the document manager and the document repository for finding and persisting pages.

12. Create a PageType form class

Following a best practice not to initialize your forms inside your controller, I created a PageType class in src/Melp/PagesBundle/Form/Type/PageType.php:

<?php
 
namespace Melp\PagesBundle\Form\Type;
 
use \Symfony\Component\Form\AbstractType;
use \Symfony\Component\Form\FormBuilder;
 
class PageType extends AbstractType
{
    function buildForm(FormBuilder $builder, array $options)
    {
        $builder
                ->add('title', 'text', array('required' => true))
                ->add('content', 'textarea')
        ;
    }
}

This class will be used inside the controller to build the forms for creating new pages and editing existing ones.

13. Implement the CRUD Controller

I chose to implement a very, very basic CRUD for the document class. The code speaks for itself. I used twig templates to render the output of the edit, create and view actions, and rely heavy on flash messages and redirects for feedback to the user.

Summarizing

Much, much easier than an RDBMS based hierarchical system of pages, much more convenient and easier to control, and crazily scalable. I think there is much future in this kind of set up. So get comfortable with it while I’ll do the same, and I hope to get even deeper into the subject some next blog.

I hope this post helped you some further if you got stuck, and is of some inspiration if you want to get started. All tips and comments are appreciated.


  1. https://github.com/jackalope/jackalope/wiki/Running-a-jackrabbit-server 

  2. See app/config/config.yml, the framework.templating section 

  3. A small introduction is given to this in github.com/doctrine/phpcr-odm 

This entry was posted in Development and tagged , , , . Bookmark the permalink. Trackbacks are closed, but you can post a comment.

12 Comments

  1. Posted July 26, 2011 at 22:50 | Permalink

    Thanks for this post, its really detailed. You should promote this post on the symfony-cmf-devs mailinglist. The PageBundle looks like a good first implementation of a Symfony CMF based CMS :-)

  2. Posted July 26, 2011 at 23:01 | Permalink

    awesome write up. are you coming to the CMF camp this week? either way we need to push forward with documentation and example code so your blog post is invaluable. thanks for being part of the PHPCR effort!

  3. Posted July 27, 2011 at 08:10 | Permalink

    few things, I know you use wp so please install the subscribe to comments thing so i can get notified whenever the discussion flares other thing is perhaps doing a hybrid approach would be nicer i will post a blog post about doing it with deps but basically is the same with SE it is too late for now to try the setup but I will first thing tomorrow, so I am sort of confused as i come from the wordpress side, a CMS would work much better with this approach with jackrabbit right? I know of some blog projects like PSSBlog which tries to match the mapping of wp, I am talking about the importance to transfer the info to this approach, and also another blog from dustin10 on github.com/dustin10 whose key feature is that he is using all FOS Bundles which makes it a powerful thing. Any thoughts?

  4. Posted July 27, 2011 at 08:13 | Permalink

    the last thing I missed was a feedburner via email subscription, check my blog I have one, I can even make one myself of your blog and subscribe :) that is what i will do, I can’t wait for you to do it :) keep blogging on your next steps, we are following, and another thing, take that captcha out! just upgrade wp, you dont need that thing it is too hard!

  5. Posted July 27, 2011 at 09:49 | Permalink

    awesome post, i will look into your PagesBundle!

    one remark on point 3: if you init the submodules of phpcr-odm recursively, you will get jackalope and phpcr in versions that should be compatible. phpcr-odm and jackalope master might diverge sometimes.

    did you see the cmf NavigationBundle and ContentBundle? there is a CMF Sandbox that integrates the phpcr doctrine layer with jackalope and everything, as well as the cmf bundles with a symfony2 installation. i do hope that during the cmf workshop in italy we bring that sandbox up to date.

  6. drm
    Posted July 27, 2011 at 22:39 | Permalink

    Thanks for all your comments, guys. I don’t have time examining all your suggestions right now, but I will any time soon.

    @cordoval, I enabled the notify plugin and disabled the captcha. If, all of the sudden, I am now eligible for discount on viagra, I’ll know where to thank you ;) Thanks for the tips. Not sure what you mean by your feedburner suggestion, though …?

  7. Posted July 29, 2011 at 18:38 | Permalink

    about the feedburner thing you can cut a feedburner then go under publicize tab on feedburner.google.com and then activate delivery over email then you will understand there is a widget there you can add to your blog

    I have it as link at http://www.craftitonline.com just finished doing a post on phpcr cmf-sandbox install

  8. Posted January 27, 2012 at 00:00 | Permalink

    Hello. I’ve found a typo at the step 3. In the line git clone git://github.com/doctrine/phpcd-odm.git is phpcr-odm and not phpcd-odm

    At the next line the URL for DoctrinePHPCRBundle has changed fromgit://github.com/symfony-cmf/DoctrinePHPCRBundle.git to git://github.com/doctrine/DoctrinePHPCRBundle.git

    I also think that the code at the step 5 has to changed somehow but I haven’t figured out how yet. Hint ?

  9. Posted February 4, 2012 at 15:10 | Permalink

    note that we are building official docs now at https://github.com/symfony-cmf/symfony-cmf-docs

    one thing missing in this tutorial is to register the annotations for phpcr-odm. see also http://groups.google.com/group/symfony-cmf-devs/browse_thread/thread/3282d301336dc8f9

  10. Posted February 21, 2013 at 01:30 | Permalink

    Thanks for sharing your info. I truly appreciate your efforts and I will be waiting for your next write ups thanks once again.

  11. Pallavi
    Posted May 2, 2013 at 11:40 | Permalink

    Hi,

    I am new to the Symfony and CMF too.. Please help me to get out of this issue is urgent.. “None of the chained routers were able to generate route: Route ‘/cms/simple’ not found, /cms/simple”

    Thanks, In Advance

  12. Posted April 11, 2014 at 12:38 | Permalink

    Its like you read my mind! You seem to know a lot about this, like you wrote the book in it or something. I think that you could do with a few pics to drive the message home a bit, but instead of that, this is wonderful blog. An excellent read. I will certainly be back.

Post a Comment

Your email is never published nor shared.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">