PHP Content Repository

phpcr.github.com

Data in a CMS is mostly unstructured

RDBMS are not a good fit, hurray for NoSQL

like fitting a square into a circle

CMS content as a tree/graph

NoSQL not a good fit, hurray for Graph DBs

content graph

CMS should handle versioning

multiple versions

Complexity shouldn't overwhelm developers

Need a solution that can scale both from small to large projects and we rather not reinvent the wheel!

Enter PHPCR

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

About PHPCR

PHPCR implementations

PHPCR Features


* Not yet implemented in Jackalope-Jackrabbit

PHPCR concepts

Hierarchical document store

Repository Content

<jcr:root>
  <cms>
    <pages>
      <home title="Hello">
        <block title="News"
          content="Today: PHPCR presentation"/>
      </home>
      <contact title="Contact"
        content="phpcr-users@groups.google.com"/>
    </pages>
  </cms>
</jcr:root>

PHPCR

PHPCR class interaction diagrammSource: phpcr.github.com

PHPCR code examples

Creating a session

use PHPCR\SimpleCredentials;

// start of implementation specific configuration //
use Jackalope\RepositoryFactoryJackrabbit as Factory;
$parameters = array(
    'jackalope.jackrabbit_uri'
        => 'http://localhost:8080/server',
);
$repository = Factory::getRepository($parameters);
// end of implementation specific configuration //

$creds = new SimpleCredentials('admin','admin');
$session = $repository->login($creds, 'default');
            

CRUD operations

$root = $session->getRootNode();

// Nodes must be added as child of another node
$node = $root->addNode('test', 'nt:unstructured');

// New node is immediately available to this session
$node = $session->getNode('/test');

// Create/update a property
$node->setProperty('prop', 'value');

// New node is now available for all sessions
$session->save();

// Delete the node and all its children
$node->remove();

// Fails if a concurrent session also changed '/test'
$session->save();
            

Tree Traversal API

$node = $session->getNode('/site/content');

foreach ($node->getNodes() as $child) {
    var_dump($child->getName());
}

// or in short
foreach ($node as $child) {
    var_dump($child->getName());
}

// filter on node names
foreach ($node->getNodes('di*') as $child) {
    var_dump($child->getName());
}
            

Versioning API

// make versionable
$node = $session->getNode('/site/content/about');
$node->addMixin('mix:versionable');
$session->save();
// create initial version
$node->setProperty('title', 'About');
$session->save();

// check-in (create version)
// and check-out (prepare for further updates)
// persisted immediately without a save() call
$vm = $session->getWorkspace()->getVersionManager();
$vm->checkpoint($node->getPath());
            

Versioning API

// update node with some changes
$node->setProperty('title', 'Ups');
$session->save();

// create another version, leave in read only state
$vm->checkin($node->getPath());

$base = $vm->getBaseVersion($node->getPath());
$current = $base->getLinearPredecessor();
$previous = $current->getLinearPredecessor();

// get snapshot of old version to look around
$frozenNode = $previous->getFrozenNode();
echo $frozenNode->getProperty('title'); // About

// set the live data back to what is in this version
$vm->restore(true, $previous);

$node = $session->getNode('/site/content/about');
echo $node->getProperty('title'); // About
            

Search via SQL2 API

$qm = $workspace->getQueryManager();

// unlike SQL, in SQL2 "*" does not return all
// columns but at least the path and match score
$sql = "SELECT * FROM [nt:unstructured]
    WHERE [nt:unstructured].type = 'nav'
    AND ISDESCENDANTNODE('/some/path')
    ORDER BY score, [nt:unstructured].title";
$query = $qm->createQuery($sql, 'JCR-SQL2');
$query->setLimit($limit);
$query->setOffset($offset);
$queryResult = $query->execute();

foreach ($queryResult->getNodes() as $node) {
    var_dump($node->getPath());
}
            

Search via Fluent Query API

(With phpcr-utils)

$qm = $workspace->getQueryManager();
$factory = $qm->getQOMFactory();

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

Concurrent write operations

$test = $session->getNode('/test');
$test1 = $session1->getNode('/test');

$test->remove();
$test1->setProperty('prop', 'value');

$session->save();

// this will fail as the node is gone
$session1->save();
            

Locking

// prepare
$test = $session->getNode('/test');
$test->addMixin('mix:lockable');
$session->save();

// use
$lm = $session->getWorkspace()->getLockManager();
// lock node and children for 5 seconds
$lock = $lm->lock('/test', true, true, 5);

$test1 = $session1->getNode('/test');

// here you will get an exception that node is locked
$test1->setProperty('prop', 'value');
            

Quality

A test suite for PHPCR makes sure all implementations interpret the specification the same way.

Quality

Test results using Jackalope with the Jackrabbit backend.

......................................S.........S.....II.....   61 / 1222 (  4%)
...........S.S...............................................  122 / 1222 (  9%)
...........SS.......SS.S.......S.............................  183 / 1222 ( 14%)
.............................................................  244 / 1222 ( 19%)
.............................................................  305 / 1222 ( 24%)
.............................................................  366 / 1222 ( 29%)
.............................................................  427 / 1222 ( 34%)
..............................S.........SSS..............I.I.  488 / 1222 ( 39%)
I.I.........................I...................I..I........S  549 / 1222 ( 44%)
I............................................................  610 / 1222 ( 49%)
.....S.......I.....................S..S......................  671 / 1222 ( 54%)
.................SS...............SSSS..S....................  732 / 1222 ( 59%)
.......................................................SSSSS.  793 / 1222 ( 64%)
..........................S.............I......S...........I.  854 / 1222 ( 69%)
.............................SS..................S...........  915 / 1222 ( 74%)
.............................................................  976 / 1222 ( 79%)
............................................................. 1037 / 1222 ( 84%)
...............................S............................. 1098 / 1222 ( 89%)
...........SSSS...........SSS................................ 1159 / 1222 ( 94%)
........................................SSSSSS............... 1220 / 1222 ( 99%)
.

Time: 03:23, Memory: 93.50Mb

OK, but incomplete or skipped tests!
Tests: 1212, Assertions: 6797, Incomplete: 17, Skipped: 49.
            

Doctrine PHPCR ODM

Object-Document Mapper

Doctrine PHPCR ODM

Document class

namespace Foo;

use Doctrine\ODM\PHPCR\Mapping as PHPCRODM;

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

    /**
     * @PHPCRODM\ParentDocument
     */
    public $parent;

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

    /** @PHPCRODM\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

/**
 * 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;
            

Doctrine PHPCR ODM Symfony2 Bundle

PHPCR ODM Symfony2 Configuration

doctrine_phpcr:
  # configure the PHPCR session
  session:
    backend:
      type: doctrinedbal
      connection: doctrine.dbal.default_connection
    workspace: default

  # enable the ODM layer
  odm:
    auto_mapping: true
    auto_generate_proxy_classes: %kernel.debug%
    locales:
      en:
        - en
        - fr
      fr:
        - fr
        - en

Conclusions

Not all data fits well in PHPCR/JCR

Door swings both ways, so remember

like fitting a square into a circle

Play with it today!

PHPCR Tutorial



See it in action!

Symfony2 CMF sandbox

PHPCR in real live

Next steps

Many individuals contribute to the effort


  • 0x616469 (Adrian Schlegel)
  • adou600 (Adrien Nicolet)
  • beberlei (Benjamin Eberlei)
  • bergie (Henri Bergius)
  • bmatzner (Bernd Matzner)
  • brki (Brian King)
  • chirimoya (Thomas Schedler)
  • chregu (Christian Stocker)
  • cordoval (Luis Cordova)
  • craigmarvelley (Craig Marvelley)
  • cryptocompress (Crypto Compress)
  • damz (Damien Tournoud)
  • dbojdo (Daniel Bojdo)
  • dbu (David Buchmann)
  • dotZoki (Zoran)
  • ebi (Tobias Ebnöther)
  • fabian (Fabian Vogler)
  • flojon (Jonas Flodén)
  • iambrosi (Ismael Ambrosi)
  • jakuza (Jacopo Romei)
  • justinrainbow (Justin Rainbow)
  • k-fish/kdambekalns (Karsten Dambekalns)
  • krizon (Kristian Zondervan)
  • lapistano (Bastian Feder)
  • lsmith77 (Lukas K. Smith)
  • mdekrijger
  • micheleorselli (Michele Orselli)
  • nacmartin (Nacho Martín)
  • nicam (Pascal Helfenstein)
  • Ocramius (Marco Pivetta)
  • ornicar (Thibault Duplessis)
  • pajooh (arash)
  • petesiss (Pete Sisson)
  • piotras
  • pitpit (Damien Pitard)
  • rande (Thomas)
  • richardmiller (Richard Miller)
  • rndstr (Roland Schilter)
  • robertlemke (Robert Lemke)
  • sebastien-roch (Sébastien Roch)
  • Seldaek (Jordi Boggiano)
  • simensen (Beau Simensen)
  • sixty-nine (Daniel Barsotti)
  • sjopet
  • starkj (Johannes Stark)
  • stof (Christophe Coevoet)
  • uwej711 (Uwe Jäger)
  • vedranzgela (Vedran Zgela)
  • videlalvaro (Alvaro Videla)
  • ...

Many projects have expressed interest

Symfony2 CMF, Midgard, Typo3, Nooku, ezPublish, Drupal

Thank you

Please give me feedback:

@dbu on Twitter, or just david@liip.ch