1/41

PHP Content Repository

Porting a Java standard to PHP

Content Repository

  • Do not re-invent the content storage wheel
  • Write applications once, switch storage backends

The JCR model

Hierarchical document store

  • All content is stored in a tree of nodes
  • Nodes have a name and a type
  • Nodes have child nodes and properties holding values
  • Values can be strings or numbers, but also binary file data or references to other nodes

Repository Content

01<jcr:root>
02  <cms>
03    <pages>
04      <home title="Hello">
05        <block title="News"
06          content="Today: PHPCR presentation"/>
07      </home>
08      <contact title="Contact"
09        content="phpcr-users@groups.google.com"/>
10    </pages>
11  </cms>
12</jcr:root>

PHPCR class interaction diagrammSource: phpcr.github.com

Nodes

  • A Node is a named container located under a parent node
  • Think of XML elements
  • Can be created, deleted, modified, copied...
  • Path is the parent path plus the node name
     
    • Path: /cms/pages/home
       
    • Parent path: /cms/pages
    • Node name: home

Properties

  • A Node has named Properties with values
  • Think of XML attributes
  • Data types
    • STRING, URI, BOOLEAN, LONG, DOUBLE, DECIMAL, BINARY, DATE, NAME, PATH, WEAKREFERENCE, REFERENCE
  • (WEAK)REFERENCE: Create links to other nodes
     
  • Node and property names can use namespaces to avoid name collisions
    • jcr:created, jcr:mimeType, phpcr:class

Primary Node Types

  • Defines allowed names and types for properties and child nodes
  • Every node must have a primary node type
  • Use nt:unstructured to allow anything
  • Some other built-in types: nt:address, nt:folder, nt:file ...
  • Define custom types to control your "schema"

Mixin Node Types

  • No multiple inheritance for primary type.
  • Mixins bring "traits" to your nodes.
  • Mixin node types can be assigned to a node during that node's lifetime

JCR / PHPCR Features

  • Tree access
  • Access by UUID
  • Search nodes
  • Versioning
  • Capability discovery
  • XML import and export
  • Locking
  • Transactions
  • Permissions
  • Node Level Access Control *
  • Observation

* Not yet implemented in any PHP implementation

From Java to PHP

PHP = Java

  • Classes and Methods
  • Interfaces
  • Namespaces
  • Access Levels: private / protected / public

PHP != Java

  • Weak typing
  • No method return signature
  • PHP is lazy / Java is overzealous

Simplifications

  • No Value and ValueFactory but primitive types
  • Node::getPropertyValue to avoid instantiating even Property
  • No type specific iterators (but not plain arrays)
  • Traversable on node, property, query result and so on

Conclusions

Not all data fits well in PHPCR/JCR

  • For example aggregation is better done in an RDBMS
  • Store web store product description in PHPCR/JCR
  • Store web store inventory and orders in RDBMS
like fitting a square into a circle

Quality

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

  • Higher quality of implementations
  • Discover interpretation issues, better compatibility
  • Implementors need to write less tests of their own

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.

Play with it today!

PHPCR Tutorial



See it in action!

Symfony2 CMF sandbox

PHPCR in real live

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
  • 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 Rabaix)
  • 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 (Sjoerd Peters)
  • starkj (Johannes Stark)
  • stof (Christophe Coevoet)
  • uwej711 (Uwe Jäger)
  • vedranzgela (Vedran Zgela)
  • videlalvaro (Alvaro Videla)
  • ...

Doctrine PHPCR ODM

  • Object - Document mapper to map plain PHP objects to PHPCR data
  • Supports the PHPCR concepts like children and references

Thank you

Please give me feedback:

@dbu on Twitter, or david@liip.ch

PHPCR code examples

Creating a session

01use PHPCR\SimpleCredentials;
02 
03// start of implementation specific configuration //
04$parameters = array(
05    'jackalope.jackrabbit_uri'
06        => 'http://localhost:8080/server',
07);
08$factory = new \Jackalope\RepositoryFactoryJackrabbit();
09$repository = $factory->getRepository($parameters);
10// end of implementation specific configuration //
11 
12$creds = new SimpleCredentials('admin','admin');
13$session = $repository->login($creds, 'default');

CRUD operations

01$root = $session->getRootNode();
02 
03// Nodes must be added as child of another node
04$node = $root->addNode('test', 'nt:unstructured');
05 
06// New node is immediately available to this session
07$node = $session->getNode('/test');
08 
09// Create/update a property
10$node->setProperty('prop', 'value');
11 
12// New node is now available for all sessions
13$session->save();
14 
15// Delete the node and all its children
16$node->remove();
17 
18// Fails if a concurrent session also changed '/test'
19$session->save();

Tree Traversal API

01$node = $session->getNode('/site/content');
02 
03foreach ($node->getNodes() as $child) {
04    var_dump($child->getName());
05}
06 
07// or in short
08foreach ($node as $child) {
09    var_dump($child->getName());
10}
11 
12// filter on node names
13foreach ($node->getNodes('di*') as $child) {
14    var_dump($child->getName());
15}

Versioning API

01// make versionable
02$node = $session->getNode('/site/content/about');
03$node->addMixin('mix:versionable');
04$session->save();
05// create initial version
06$node->setProperty('title', 'About');
07$session->save();
08 
09// check-in (create version)
10// and check-out (prepare for further updates)
11// persisted immediately without a save() call
12$vm = $session->getWorkspace()->getVersionManager();
13$vm->checkpoint($node->getPath());

Versioning API

01// update node with some changes
02$node->setProperty('title', 'Ups');
03$session->save();
04 
05// create another version, leave in read only state
06$vm->checkin($node->getPath());
07 
08$base = $vm->getBaseVersion($node->getPath());
09$current = $base->getLinearPredecessor();
10$previous = $current->getLinearPredecessor();
11 
12// get snapshot of old version to look around
13$frozenNode = $previous->getFrozenNode();
14echo $frozenNode->getProperty('title'); // About
15 
16// set the live data back to what is in this version
17$vm->restore(true, $previous);
18 
19$node = $session->getNode('/site/content/about');
20echo $node->getProperty('title'); // About

Search via SQL2 API

01$qm = $workspace->getQueryManager();
02 
03// unlike SQL, in SQL2 "*" does not return all columns
04// but at least the path and match score
05$sql = "SELECT * FROM [nt:unstructured]
06    WHERE [nt:unstructured].type = 'nav'
07    AND ISDESCENDANTNODE('/some/path')
08    ORDER BY score, [nt:unstructured].title";
09$query = $qm->createQuery($sql, 'JCR-SQL2');
10$query->setLimit($limit);
11$query->setOffset($offset);
12$queryResult = $query->execute();
13 
14foreach ($queryResult->getNodes() as $node) {
15    var_dump($node->getPath());
16}

Search via QOM API

01$qm = $workspace->getQueryManager();
02$factory = $qm->getQOMFactory();
03 
04// SELECT * FROM nt:file INNER JOIN nt:folder ON ISCHILDNODE(child, parent)
05$factory->createQuery(
06  $factory->join(
07    $factory->selector('nt:file'),
08    $factory->selector('nt:folder'),
09    Constants::JCR_JOIN_TYPE_INNER,
10    $factory->childNodeJoinCondition(
11        'child', 'parent')),
12  null,
13  array(),
14  array()
15);

Search via Fluent Query API

(With phpcr-utils)

01$qm = $workspace->getQueryManager();
02$factory = $qm->getQOMFactory();
03 
04// SELECT * FROM nt:unstructured
05// WHERE name NOT IS NULL
06// LIMIT 10 OFFSET 10
07$qb = new QueryBuilder($factory);
08$qb->select($factory->selector('nt:unstructured'))
09   ->where($factory->propertyExistence('name'))
10   ->setFirstResult(10)
11   ->setMaxResults(10)
12   ->execute();

Concurrent write operations

01$test = $session->getNode('/test');
02$test1 = $session1->getNode('/test');
03 
04$test->remove();
05$test1->setProperty('prop', 'value');
06 
07$session->save();
08 
09// this will fail as the node is gone
10$session1->save();

Locking

01// prepare
02$test = $session->getNode('/test');
03$test->addMixin('mix:lockable');
04$session->save();
05 
06// use
07$lm = $session->getWorkspace()->getLockManager();
08// lock node and children for 5 seconds
09$lock = $lm->lock('/test', true, true, 5);
10 
11$test1 = $session1->getNode('/test');
12 
13// here you will get an exception that node is locked
14$test1->setProperty('prop', 'value');