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.
- Get the patched Jackrabbit here.
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:
- Download Symfony 2.0 latest version
- Extract the archive in your document root, say
/var/www/
- Visit /Symfony/web/config.php and follow the instructions
- 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.
-
https://github.com/jackalope/jackalope/wiki/Running-a-jackrabbit-server ↩︎
-
See
app/config/config.yml
, theframework.templating
section ↩︎ -
A small introduction is given to this in github.com/doctrine/phpcr-odm ↩︎