melp.nl

< Return to main page

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

    #!shell
    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:

#!shell
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
<?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
<?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:

#!yaml
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:

#!shell
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:

#!shell
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:

#!shell
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
<?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
<?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:

#!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
<?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
<?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:

#!yaml
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
<?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 ↩︎


< Return to main page


You're looking at a very minimalistic archived version of this website. I wish to preserve to content, but I no longer wish to maintain Wordpress, nor handle the abuse that comes with that.