PHP Content Repository Specification

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 often organize content as a tree/graph

Most NoSQL not a good fit, hurray for Graph DBs

content graph

CMS should be able to store content versions

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

PHPCR concepts

Hierarchical document store

Nodes

Properties

Primary Node Types

Mixin Node Types

Mixin Node Type Examples

Workspaces

PHPCR

PHPCR class interaction diagrammSource: phpcr.github.com

PHPCR code examples

Connecting via PHPCR

// 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 \PHPCR\SimpleCredentials('user','pw');
$session = $repository->login($creds, 'default');
            

CRUD operations

$root = $session->getRootNode();

// Node always added as child of another node
$node = $root->addNode('test', 'nt:unstructured');
// new node is immediately available in memory
$node = $session->getNode('/test');

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

// Persist the changes, /test is now available for all sessions
$session->save();

// Delete a node and all its children
$node->remove();
$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();

$vm = $session->getWorkspace()->getVersionManager();

$node->setProperty('title', 'About');

// create initial version
$session->save();

// check-in (create version)
// and check-out (prepare for further updates)
// persisted immediately without a save() call
$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
$qb = new QueryBuilder($factory);
$qb->select($factory->selector('nt:unstructured'))
   ->where($factory->propertyExistence('name'))
   ->setFirstResult(10)
   ->setMaxResults(10)
   ->execute();
            

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.

Soitgoes:jackalope-jackrabbit lsmith$ phpunit -c tests/phpunit.xml.dist
PHPUnit 3.6.5 by Sebastian Bergmann.

.........................................SSS..............I.I   61 / 1088 (  5%)
.I.I........................I...............I..I........SI...  122 / 1088 ( 11%)
.............................................................  183 / 1088 ( 16%)
.S.......I....................S..S...........................  244 / 1088 ( 22%)
........SS...............SSSS..S.............................  305 / 1088 ( 28%)
.......................................SSSSS.................  366 / 1088 ( 33%)
.......S..............I......S..........I....................  427 / 1088 ( 39%)
....SS..................S....................................  488 / 1088 ( 44%)
.............................................................  549 / 1088 ( 50%)
.............................................................  610 / 1088 ( 56%)
......S..............................S.......................  671 / 1088 ( 61%)
...........................................SSSSSS..S.........  732 / 1088 ( 67%)
S.....II...............S.S...................................  793 / 1088 ( 72%)
......SSSS.S.......S.........................................  854 / 1088 ( 78%)
.............................................................  915 / 1088 ( 84%)
.............................................................  976 / 1088 ( 89%)
............................................................. 1037 / 1088 ( 95%)
..............................................S..

Time: 01:06, Memory: 184.50Mb

OK, but incomplete or skipped tests!
Tests: 1077, Assertions: 5979, Incomplete: 13, Skipped: 43.
            

Quality

Test results using Jackalope with the Doctrine DBAL backend.

Soitgoes:jackalope-doctrine-dbal lsmith$ phpunit -c tests/phpunit.xml.dist
Updating schema...done.
PHPUnit 3.6.5 by Sebastian Bergmann.

.......................S.S..............I.I.I.I................  63 / 709 (  8%)
........I...............I..I........SI.......................S. 126 / 709 ( 17%)
........................................S.......I.............. 189 / 709 ( 26%)
......S..S...................................SS.........S.....S 252 / 709 ( 35%)
SSS..S......SS..............................................S.. 315 / 709 ( 44%)
...........SSSSS.......S..S..S.........SS.......S....S.I......S 378 / 709 ( 53%)
..S.......IS................SSSSSSSSSSSSSSSSSSS.......S........ 441 / 709 ( 62%)
...........S................................................... 504 / 709 ( 71%)
............................................................... 567 / 709 ( 79%)
.............................................................SS 630 / 709 ( 88%)
.SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS 693 / 709 ( 97%)
SSSSSSSSSSSSI

Time: 13 seconds, Memory: 177.25Mb

OK, but incomplete or skipped tests!
Tests: 617, Assertions: 4660, Incomplete: 12, Skipped: 130.
            

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

Next steps

Many individuals contribute to the effort


  • adou600 (Adrien Nicolet)
  • beberlei (Benjamin Eberlei)
  • bergie (Henri Bergius)
  • brki (Brian King)
  • chirimoya (Thomas Schedler)
  • chregu (Christian Stocker)
  • cordoval (Luis Cordova)
  • damz (Damien Tournoud)
  • dbu (David Buchmann)
  • dotZoki (Zoran)
  • ebi (Tobias Ebnöther)
  • iambrosi (Ismael Ambrosi)
  • jakuza (Jacopo Romei)
  • justinrainbow (Justin Rainbow)
  • k-fish (Karsten Dambekalns)
  • krizon (Kristian Zondervan)
  • lapistano (Bastian Feder)
  • lsmith77 (Lukas K. Smith)
  • micheleorselli (Michele Orselli)
  • nacmartin (Nacho Martín)
  • nicam (Pascal Helfenstein)
  • Ocramius (Marco Pivetta)
  • ornicar (Thibault Duplessis)
  • piotras
  • pitpit (Damien Pitard)
  • robertlemke (Robert Lemke)
  • rndstr (Roland Schilter)
  • Seldaek (Jordi Boggiano)
  • sixty-nine (Daniel Barsotti)
  • 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