Doctrine PHPCR ODM

www.doctrine-project.org/projects/phpcr_odm

PHP Content Repository + Doctrine ODM

PHPCR (aka PHP-ified JCR specification)

Works like MongoDB or CouchDB ODM, but also includes a tree/graph, versioning API

JCR vs. RDBMS/NoSQL

PHPCR provides a standardized API that can be used by any PHP content management system to interface with any content repository.


Understanding PHPCR helps to understand the PHPCR ODM. See PHPCR slides for an introduction.

Doctrine ODM

Document class

namespace Foo;

use Doctrine\ODM\PHPCR\Mapping as PHPCR;

/** @PHPCR\Document */
class Bar
{
    /** @PHPCR\Id */
    public $id;

    /**
     * presence of this annotation changes default id strategy
     * to "parent and name" strategy
     * @PHPCR\ParentDocument
     */
    public $parent;

    /** @PHPCR\Nodename */
    public $nodename;

    /** @PHPCR\String */
    public $text;
}
            

CRUD API

// Create
$document = new Foo\Bar();
$document->parent = $dm->find(null, '/');
$document->nodename = 'test';
$document->text = 'Test text';
$dm->persist($document);
$dm->flush();

// Read
$document = $dm->find(null, '/test');

// Update
$document->text = 'foo!';
$dm->flush();

// Remove
$dm->remove($document);
$dm->flush();
            

Traversal and References

/** @PHPCR\Document */
class Bar
{
    /**
     * Map the child node named the-logo
     * @PHPCR\Child(name="the-logo")
     */
    public $logo;

    /**
     * All child nodes starting with "a".
     * @PHPCR\Children(filter="a*")
     */
    public $children;

    /** @PHPCR\ReferenceOne */
    public $reference;

    /** @PHPCR\Referrers */
    public $referrers;

    ...
}
            

Versioning API: basics

// @Document(versionable="simple")
$document = $dm->find(null, $id);

// create a new version
$dm->checkpoint($document);

// get latest 2 entries from history
$history = $dm->getAllLinearVersions($document, 2);
// returns array with version name and creation date of each version

// get a document in an old state
$version = reset($history);
$pre = $dm->findVersionByName(null, $id, $version['versionname']);
echo $pre->text;
            

Versioning API: restore and delete

$pre = $dm->findVersionByName(null, $id, $versionname);

// restore this old version
$dm->restoreVersion($pre, true);

// delete a version from the history
// (i.e. to remove illegal content from a wiki-like page)
$illegal = $dm->findVersionByName(null, $id, $v);
$dm->deleteVersion($illegal);
            

Search via SQL2 API

$sql = "SELECT * FROM [nt:unstructured]
    WHERE [nt:unstructured].text = 'foo!'
    ORDER BY [nt:unstructured].title";
$query = $dm->createQuery($sql, 'JCR-SQL2');
$query->setLimit($limit);
$query->setOffset($offset);

$collection = $dm->getDocumentsByQuery($query);
            

Search via the QueryBuilder

$qb = $dm->createQueryBuilder();
$factory = $qb->getQOMFactory();

// SELECT * FROM nt:unstructured WHERE name NOT IS NULL
$qb->select($factory->selector('nt:unstructured'))
    ->where($factory->propertyExistance('name'))
    ->setFirstResult(10)
    ->setMaxResults(10)
    ->execute();

$collection = $dm->getDocumentsByQuery($qb->getQuery());
            

Transactions

$workspace = $dm->getPhpcrSession()->getWorkspace();
$utx = $workspace->getTransactionManager();
$utx->begin();

try {
    $utx->begin();

    // do stuff with the DocumentManager
    $dm->flush();

    // do more stuff with the DocumentManager
    $dm->flush();

    $utx->commit();
} catch (\Exception $e) {
    $utx->rollback();

    // the DocumentManager is now in an inconsistent state
    unset($dm)
}

Translation

/** @PHPCR\Document(translator="attribute") */
class Article
{
    /**
     * The language this document currently is in
     * @PHPCR\Locale
     */
    public $locale;

    /**
     * Untranslated property
     * @PHPCR\Date
     */
    public $publishDate;

    /**
     * Translated property
     * @PHPCR\String(translated=true)
     */
    public $topic;

    /**
     * Language specific image
     * @PHPCR\Binary(translated=true)
     */
    public $image;
}
            

Storing translations

$article = new Article();
$article->topic = 'bonjour';
$dm->persist($article);
$dm->bindTranslation($article, 'fr');

$article->topic = 'hello';
$dm->bindTranslation($article, 'en');

$article->topic = 'hello';
// ODM tracks the locale change and
// does an implicit bindTranslation() call on flush()
$article->locale = 'de';

// flushes french, english and german versions into the database
$dm->flush();
            

Loading translations

$article = $dm->findTranslation(null, '/test', 'fr');
echo $article->topic; // 'bonjour'
$article = $dm->findTranslation(null, '/test', 'en');
echo $article->topic; // 'hello'

$article->topic = 'hello you';
// use the @Locale field if set, otherwise language document was loaded in
$dm->flush();

// load in default language
$article = $dm->find(null, '/test');

$locales = $dm->getLocalesFor($article);
var_dump($locales); // en, fr

            

Translation

Document repository (optional)

namespace Foo;

use Doctrine\ODM\PHPCR\DocumentRepository;
use Doctrine\ODM\PHPCR\Id\RepositoryIdInterface;

/** @PHPCR\Document(repositoryClass="Foo\BarRepository") */
class Bar
{
    /** @PHPCR\Id(strategy="repository") */
    public $id;
    ...

class BarRepository
    extends DocumentRepository
    implements RepositoryIdInterface
{
    public function generateId(Bar $document)
    {
        return '/'.$document->getName();
    }
}
            

Access underlying PHPCR API

$session = $dm->getPhpcrSession();
$node = $session->getNode('/foo/bar/ding/dong');

$i = 0;
$breadcrumb = array();

// note this code doesn't handle graphs
do {
    $i++;
    $parent = $node->getAncestor($i);
    $breadcrumb[$parent->getPath()] =
        $parent->getPropertyValue('label');
} while ($parent != $node);
            

Next steps

Github projects

Resources