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>

Nodes

Properties

Primary Node Types

Mixin Node Types

Mixin Node Type Examples

Workspaces

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 QOM API

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

// SELECT * FROM nt:file INNER JOIN nt:folder ON ISCHILDNODE(child, parent)
$factory->createQuery(
  $factory->join(
    $factory->selector('nt:file'),
    $factory->selector('nt:folder'),
    Constants::JCR_JOIN_TYPE_INNER,
    $factory->childNodeJoinCondition(
        'child', 'parent')),
  null,
  array(),
  array()
);
            

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.
            

Quality

Test results using Jackalope with the Doctrine DBAL backend.

....................S.........S.....II................S.S....   61 / 1185 (  5%)
......................................................SS.....  122 / 1185 ( 10%)
.........SS.S.......S........................................  183 / 1185 ( 15%)
.............................................................  244 / 1185 ( 20%)
.............................................................  305 / 1185 ( 25%)
.............................................................  366 / 1185 ( 30%)
.............................................................  427 / 1185 ( 36%)
......S...........S..............I.I.I.I.....................  488 / 1185 ( 41%)
...I...............I..I........SI............................  549 / 1185 ( 46%)
.....................................S.......I...............  610 / 1185 ( 51%)
......S..S...................................SS..............  671 / 1185 ( 56%)
.SSSS..S......SS............SSSSSS...........................  732 / 1185 ( 61%)
......S.............SSSSS..........................S.........  793 / 1185 ( 66%)
...S.I......S...........I............................SS......  854 / 1185 ( 72%)
................................S............................  915 / 1185 ( 77%)
.............................................................  976 / 1185 ( 82%)
............................................................. 1037 / 1185 ( 87%)
.....................................SSS...SSSSS.SSSSSSSSSSSS 1098 / 1185 ( 92%)
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS 1159 / 1185 ( 97%)
S.S..SI................S

Time: 48 seconds, Memory: 212.50Mb

OK, but incomplete or skipped tests!
Tests: 1102, Assertions: 6251, Incomplete: 14, Skipped: 128.
            

Doctrine PHPCR ODM

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)
  • ...

Several companies and organisations are investing into the effort

Liip, Ideato, Nemein, IKS

Many projects have expressed interest

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

Github projects

Resources