Getting Started

Note

Difference from the ORM

Doctrine ORM knows several models of developing. If you know the ORM, please note that with PHPCR-ODM you can only develop “Code First”. We do not (yet) have any generator to build model classes from a content repository.

As PHPCR allows NoSQL like data storage, we speak of Documents rather than Entities to stress the fact that there need not be a rigid database model.

Doctrine PHPCR-ODM is an object-document mapper (ODM) for PHP 5.3.0+ that provides transparent persistence for PHP objects. It uses the Data Mapper pattern at the heart of this project, aiming for a complete separation of the domain/business logic from the persistence in a content repository. The benefit of Doctrine for the programmer is the ability to focus solely on the object-oriented business logic and worry about persistence only as a secondary task. This doesn’t mean persistence is not important to Doctrine, however it is our belief that there are considerable benefits for object-oriented programming if persistence and documents are kept perfectly separated.

Starting with the object-oriented model is called the Code First approach to Doctrine.

What are Documents?

Documents are lightweight PHP Objects that don’t need to extend any abstract base class or interface. A document class must not be final or contain final methods. Additionally it must not implement clone nor wakeup or do so safely.

See the architecture chapter for a full list of the restrictions that your entities need to comply with.

A document contains persistable properties. A persistable property is an instance variable of the document that is saved into and retrieved from the content repository by Doctrine’s data mapping capabilities.

An Example Model: Document Management

  • Documents have a title and a content
  • Documents are arranged in a tree
  • Documents can reference other documents

We do not build a web interface but simple run scripts on the command line to keep this example simple.

Note

This is a simplistic document manage to illustrate the PHPCR-ODM features. If you want to build a custom Web Content Management System, we recommend looking into the Symfony CMF which is a content management framework built on top of Doctrine PHPCR-ODM and Symfony2.

Setup Project

Create a file composer.json in your project directory.

{
    "minimum-stability": "dev",
    "require": {
        "doctrine/phpcr-odm": "~1.2",
        "jackalope/jackalope-doctrine-dbal": "~1.1"
    },
    "autoload": {
      "psr-0": { "Demo\\": "src/" }
    }
}

Then run the following commands on your command line

$ curl -s http://getcomposer.org/installer | php --
$ php composer.phar install

This will download the dependencies into the vendor/ folder and generate vendor/autoload.php

Now we bootstrap Doctrine PHPCR-ODM. Create a file called bootstrap.php in your project root directory:

<?php
// bootstrap.php

$vendorDir = __DIR__.'/vendor';

$file = $vendorDir.'/autoload.php';
if (file_exists($file)) {
    $autoload = require_once $file;
} else {
    throw new RuntimeException('Install dependencies with composer.');
}

$params = array(
    'driver'    => 'pdo_mysql',
    'host'      => 'localhost',
    'user'      => 'root',
    'password'  => '',
    'dbname'    => 'phpcr_odm_tutorial',
);

$workspace = 'default';
$user = 'admin';
$pass = 'admin';

/* --- transport implementation specific code begin --- */
// for more options, see https://github.com/jackalope/jackalope-doctrine-dbal#bootstrapping
$dbConn = \Doctrine\DBAL\DriverManager::getConnection($params);
$parameters = array('jackalope.doctrine_dbal_connection' => $dbConn);
$repository = \Jackalope\RepositoryFactoryDoctrineDBAL::getRepository($parameters);
$credentials = new \PHPCR\SimpleCredentials(null, null);
/* --- transport implementation specific code  ends --- */

$session = $repository->login($credentials, $workspace);

/* prepare the doctrine configuration */
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ODM\PHPCR\Mapping\Driver\AnnotationDriver;
use Doctrine\ODM\PHPCR\Configuration;
use Doctrine\ODM\PHPCR\DocumentManager;

AnnotationRegistry::registerLoader(array($autoload, 'loadClass'));

$reader = new AnnotationReader();
$driver = new AnnotationDriver($reader, array(
    // this is a list of all folders containing document classes
    'vendor/doctrine/phpcr-odm/lib/Doctrine/ODM/PHPCR/Document',
    'src/Demo',
));

$config = new Configuration();
$config->setMetadataDriverImpl($driver);

$documentManager = DocumentManager::create($session, $config);

return $autoload;

To enable the command line, copy the cli-config.<implementation>.php.dist to cli-config.php in your vendor directory and adjust it to match your bootstrap.php. Or better, remove the duplicate code and include cli-config.php from your bootstrap.php file.

If you want it in the root directory, configure the composer bin-dir to bin:

"config": {
    "bin-dir": "bin"
}

Building the model

Models are plain PHP classes. Note that you have several ways to define the mapping. For easy readability, we use the annotation mapping in this tutorial:

<?php
// src/Demo/Document.php
namespace Demo;

use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;

/**
 * @PHPCR\Document
 */
class MyDocument
{
    /**
     * @PHPCR\Id
     */
    private $id;
    /**
     * @PHPCR\ParentDocument
     */
    private $parent;
    /**
     * @PHPCR\Nodename
     */
    private $name;
    /**
     * @PHPCR\Children
     */
    private $children;
    /**
     * @PHPCR\String
     */
    private $title;

    /**
     * @PHPCR\String
     */
    private $content;

    public function getId()
    {
        return $this->id;
    }
    public function getChildren()
    {
        return $this->children;
    }
    public function setParent($parent)
    {
        $this->parent = $parent;
    }
    public function setName($name)
    {
        $this->name = $name;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }
    public function getTitle()
    {
        return $this->title;
    }
    public function setContent($content)
    {
        $this->content = $content;
    }
    public function getContent()
    {
        return $this->content;
    }
}

If you are familiar with Doctrine ORM, this code should look pretty familiar to you. The only important difference are the hierarchy related annotations ParentDocument, Name and Children. In PHPCR, data is stored in trees. Every document has a parent and its own name. The id is built from this structure, resulting in path strings. The recommended way to generate the id is by assigning a name and a parent to the document. See the section on identifier strategies in the reference chapter Objects and Fields for other possibilities.

Note

PHPCR-ODM provides default classes for the standard PHPCR node types nt:file, nt:folder and nt:resource, as well as a GenericDocument for when a PHPCR node can not be mapped to a specific document. See the classes in lib/Doctrine/ODM/PHPCR/Document/

Initialize the repository

With jackalope-doctrine-dbal, you need to run the following command to init the database:

./vendor/bin/phpcrodm jackalope:init:dbal

Then, regardless of the PHPCR implementation you use, you need to run another command to let Doctrine set up the repository for using it:

./vendor/bin/phpcrodm doctrine:phpcr:register-system-node-types

Storing documents

We write a simple PHP script to generate some sample data:

<?php
// src/generate.php
require_once '../bootstrap.php';

// get the root node to add our data to it
$rootDocument = $documentManager->find(null, '/');

// create a new document
$doc = new \Demo\Document();
$doc->setParent($rootDocument);
$doc->setName('doc');
$doc->setTitle('My first document');
$doc->setContent('The document content');

// create a second document
$childDocument = new \Demo\Document();
$childDocument->setParent($doc);
$childDocument->setName('child');
$childDocument->setTitle('My child document');
$childDocument->setContent('The child document content');


// make the documents known to the document manager
$documentManager->persist($doc);
$documentManager->persist($childDocument);

// store all changes, insertions, etc. with the storage backend
$documentManager->flush();

Note

In real projects, you should look into the doctrine fixtures to script generating content.

Reading documents

This script will simply echo the data to the console:

<?php
// src/read.php
require_once '../bootstrap.php';

$doc = $documentManager->find(null, "/doc");

echo 'Found '.$doc->getId() ."\n";
echo 'Title: '.$doc->getTitle()."\n";
echo 'Content: '.$doc->getContent()."\n";

The DocumentManager will automatically determine the document class when you pass null as first argument to find().

Tree traversal

PHPCR is a tree based store. Every document must have a parent, and can have children. We already used this when creating the document. The @PHPCR\ParentDocument maps the parent of a document and is used to determine the position in the tree, together with @PHPCR\Nodename.

As the children of our sample document are mapped with @PHPCR\Children, we can traverse them:

<?php

$doc = $documentManager->find(null, "/doc");

foreach($doc->getChildren() as $child) {
    if ($child instanceof \Demo\Document) {
        echo 'Has child '.$child->getId() . "\n";
    } else {
        echo 'Unexpected child '.get_class($child)."\n";
    }
}

Caution

Children can be of any class. Be careful when looping over children to be sure they are of the expected class.

Even if children are not mapped, you can use the document manager to get all flushed children of a document:

<?php

$children = $documentManager->getChildren($parent);

Note

Difference from ORM

While with the ORM, the natural thing to get data is to query, with PHPCR-ODM the natural way is to use the hierarchy, that is parent-child relations.

If you need to query, see Querying in the Working with Objects section.

Add references

PHPCR-ODM supports arbitrary links between documents. The referring document does not need to know what class it links to. Use @PHPCR\ReferenceOne resp. @PHPCR\ReferenceMany to map the link to a document or a collection of links to documents.

You can also map the inverse relation. @PHPCR\Referrers needs the referring class but can be used to add referencing documents. @PHPCR\MixedReferrers maps all documents referencing this document, but is readonly.

Lets look at an example of document A referencing B:

<?php
// src/Demo/A.php
namespace Demo;

use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;

/**
 * @PHPCR\Document
 */
class A
{
    /**
     * @PHPCR\ReferenceOne
     */
    private $ref;

    ...
}

/**
 * @PHPCR\Document
 */
class B
{
    /**
     * @PHPCR\Referrers(referringDocument="Demo\A", referencedBy="ref")
     */
    private $referrers;
}

We can now create a reference with the following code:

<?php

$parent = $dm->find(null, '/');
$a = new A();
$a->setParent($parent);
$a->setNodename('a');
$dm->persist($a);
$b = new B();
$b->setParent($parent);
$b->setNodename('b');

$a->setRef($b);

$dm->flush();
$dm->clear();

$b = $dm->find(null, '/b');

// output Demo\A
var_dump(get_class($b->getReferrers()));

If referrers are not mapped on a document, you can use the document manager to get all flushed referrers of a document:

<?php

$referrers = $documentManager->getReferrers($b);

Removing documents

To delete a document, call the remove method on the DocumentManager:

<?php
// src/manipulate.php
require_once '../bootstrap.php';

// remove a document
$doc = $documentManager->find(null, '/doc');
$documentManager->remove($doc);

// persist all operations
$documentManager->flush();

Other helpful methods on the DocumentManager

You can move a document to a different path with the move method. Alternatively, you can assign a different Parent and/or Nodename to move by assignment. The latter is for example handy with Symfony2 forms:

<?php
// src/manipulate.php
require_once '../bootstrap.php';

// we move a node
$child = $documentManager->find(null, '/doc/child');
$documentManager->move($child, '/newpath');

// persist all operations
$documentManager->flush();

Conclusion

This tutorial is over here, I hope you had fun.

Additional details on all the topics discussed here can be found in the respective manual chapters.

Fork me on GitHub