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


(*) Defined by the API but not yet implemented for PHP

PHPCR concepts

Hierarchical document store

Workspaces

Nodes

Properties

Primary Node Types

Mixin Node Types

Mixin Node Type Examples

PHPCR

PHPCR class interaction diagramm Source: phpcr.github.com

PHPCR code examples

Connecting via PHPCR

                // start of implementation specific configuration //
                $factory = new \Jackalope\RepositoryFactoryJackrabbit();
                $parameters = array(
                    'jackalope.jackrabbit_uri'
                        => 'http://localhost:8080/server'
                );
                // end of implementation specific configuration //

                $repository = $factory->getRepository($parameters);
                $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');

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

                // Persist the changes
                $session->save();

                // Delete a node and all its children
                $node->remove();
                $session->save();
            

Tree API

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

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

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

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

Tree API

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

                $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);
            

Versioning API

                // make versionable
                $node = $session->getNode('/site/content/about');
                $node->addMixin('mix:versionable');
                $session->save();

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

                // create initial version
                $node->setProperty('label', 'About');
                $session->save();
                $vm->checkpoint($node->getPath());
            

Versioning API

                // update version
                $node->setProperty('label', 'Ups');
                $session->save();
                $vm->checkpoint($node->getPath());

                $base = $vm->getBaseVersion($node->getPath());
                $previous = array_pop($base->getPredecessors());
                $vm->restore(true,
                             $previous->getName(),
                             $node->getPath()
                );

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

Search via SQL2 API

                $queryManager = $workspace->getQueryManager();

                $sql = "SELECT * FROM [nt:unstructured]
                    WHERE [nt:unstructured].type = 'nav'
                    ORDER BY [nt:unstructured].title";
                $query =
                    $queryManager->createQuery($sql, 'JCR-SQL2');
                $query->setLimit($limit);
                $query->setOffset($offset);
                $queryResult = $query->execute();

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

Quality

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

                david@wintermute:~/liip/jackalope> phpunit -c tests/
PHPUnit 3.5.14 by Sebastian Bergmann.

S..II............S...................SSSSSSSSSSSSSSSS..........  63 / 702 (  8%)
.............................S......S..................S.SSSSSS 126 / 702 ( 17%)
...............SSS.............I.I.I.I.....I.III..SS.SSS..S.... 189 / 702 ( 26%)
.......................................SSSSSS.S................ 252 / 702 ( 35%)
.I...............I..I........SI..................S.......I..... 315 / 702 ( 44%)
...............S............................................... 378 / 702 ( 53%)
..........................................I..S................. 441 / 702 ( 62%)
I.......II..................S.................................. 504 / 702 ( 71%)
............................................................... 567 / 702 ( 80%)
............................................................... 630 / 702 ( 89%)
..................................I....I......S................ 693 / 702 ( 98%)
..SSS...

Time: 01:06, Memory: 70.50Mb

OK, but incomplete or skipped tests!
Tests: 701, Assertions: 4299, Incomplete: 21, Skipped: 52.
            

Doctrine PHPCR ODM

Doctrine PHPCR ODM

Doctrine PHPCR ODM

JCR vs. RDBMS/NoSQL Source: cmf.symfony.com

ODM Document class

                use Doctrine\ODM\PHPCR\Mapping as PHPCR

                /** @PHPCR\Document(alias="nav", repositoryClass="NavigationRepository") */
                class Navigation
                {
                    /** @PHPCR\Id(strategy="repository") */
                    public $id;

                    /** @PHPCR\Children */
                    public $children;

                    /** @PHPCR\String(name="label") */
                    private $label;

                    private $internal;

                    public function getLabel()
                    {
                        return $this->label;
                    }

                    public function setLabel($label)
                    {
                        $this->label = $label;
                    }
                }
            

ODM Document repository (optional)

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

                class NavigationRepository
                    extends DocumentRepository
                    implements RepositoryIdInterface
                {
                    /**
                     * Generate a document id
                     *
                     * @param Navigation $doc
                     * @return string
                     */
                    public function generateId(Navigation $doc)
                    {
                        return '/site/content/'.$doc->getLabel();
                    }
                }
            

ODM CRUD API

                // Create
                $doc = new Navigation();
                $doc->setLabel($label)
                // no active record. newly created document needs
                // to be registered it with the document manager
                $docManager->persist($doc);
                $docManager->flush();
                $id = $doc->id;

                // Read
                $repo = $docManager->getRepository('Navigation');
                $doc = $repo->find($id);

                // Update
                $doc->setLabel('home');
                // no need to call persist() here, document is
                // already known to the document manager.
                $docManager->flush();

                // Remove
                $docManager->remove($doc);
                $docManager->flush();
            

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

Eat your own dog food

Liip uses PHPCR and the Symfony CMF for their new website

Many individuals contribute to the effort


  • beberlei (Benjamin Eberlei)
  • bergie (Henri Bergius)
  • brki (Brian King)
  • chirimoya (Thomas Schedler)
  • chregu (Christian Stocker)
  • dbu (David Buchmann)
  • ebi (Tobias Ebnöther)
  • jakuza (Jacopo Romei)
  • justinrainbow (Justin Rainbow)
  • k-fish (Karsten Dambekalns)
  • lapistano (Bastian Feder)
  • lsmith77 (Lukas K. Smith)
  • micheleorselli (Michele Orselli)
  • nacmartin (Nacho Martín)
  • nicam (Pascal Helfenstein)
  • ornicar (Thibault Duplessis)
  • piotras
  • robertlemke (Robert Lemke)
  • rndstr (Roland Schilter)
  • Seldaek (Jordi Boggiano)
  • sixty-nine (Daniel Barsotti)
  • uwej711 (Uwe Jäger)
  • vedranzgela (Vedran Zgela)

Several companies and organisations are investing into the effort

Liip, Ideato, Nemein, IKS

Many projects have expressed interest

Symfony2 CMF, Midgard, Typo3, Nooku, ez Publish, Drupal

Github projects

Resources