Doctrine2 PHPCR ODM
PHP Shell
Pull request Compare This branch is 1752 commits behind doctrine:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
bin
data/share
lib
tests
.gitignore
.gitmodules
.travis.yml
LICENSE
README.md
cli-config.doctrine_dbal.php.dist
cli-config.jackrabbit.php.dist
cli-config.midgard_mysql.php.dist
cli-config.midgard_sqlite.php.dist

README.md

PHPCR ODM for Doctrine2

Current Status

TODO

  • have the register-system-node-types command provide api conform node type definition as well to support other implementations
  • write documentation
  • expand test suite
  • translations
    • make it work without the @Locale field too (store locale in meta instead on document instance)
    • provide a method to get a detached translated document so the relations can be translated automatically

Preconditions

Installation

If you use the PHPCR ODM Symfony Bundle, please look into the README of the bundle. This documentation explains how to use PHPCR ODM outside of symfony, which requires some manual initialization.

Clone the repository and initialize all dependencies (submodules)

git clone git://github.com/doctrine/phpcr-odm.git
cd phpcr-odm
git submodule update --init --recursive

Install a PHPCR provider

PHPCR ODM uses the PHP Content Repository API for storage. You need to install one of the available providers:

Install Jackrabbit

Jackalope with the Jackrabbit backend is the only PHPCR implementation that is enough feature complete for the PHPCR ODM.

Follow Running Jackrabbit Server from the Jackalope wiki.

Install Midgard2 PHPCR

Midgard2 is a PHPCR provider that provides most of the functionality needed for PHPCR ODM, and can persist your content in typical relational databases like SQLite and MySQL. Midgard2 only needs a PHP extension to run. On typical Linux setups getting the extension is as easy as:

$ sudo apt-get install php5-midgard2

Enable the console

The console provides a bunch of useful commands:

# in the phpcr-odm root directoy
cp cli-config.php.dist cli-config.php
# edit the file and adjust if needed - the defaults expect all submodules in place

Now running php bin/phpcr will show you a list of the available commands. php bin/phpcr help <cmd> displays additional information for that command.

Register the phpcr:managed node type

PHPCR ODM uses a custom node type to track meta information without interfering with your content. We provide a command that makes it trivial to register this type and the phpcr namespace.

php bin/phpcr doctrine:phpcr:register-system-node-types

Bootstrapping

Set up autoloading

For an inspiration for the autoloading, have a look at cli-config.php.dist. You need to make sure that the following paths are autoloaded (all paths relative to the phpcr-odm root directory):

'Doctrine\ODM'    => 'lib',
'Doctrine\Common' => 'lib/vendor/doctrine-common/lib',
'Symfony\Component\Console' => 'lib/vendor/jackalope/lib/phpcr-utils/lib/vendor',
'Symfony'         => 'lib/vendor,
'PHPCR\Util'      => 'lib/vendor/jackalope/lib/phpcr-utils/src',
'PHPCR'           => 'lib/vendor/jackalope/lib/phpcr/src',
'Jackalope'       => 'lib/vendor/jackalope/src',
'Midgard\PHPCR'   => 'lib/vendor/Midgard/PHPCR/src',
'Doctrine\DBAL'   => 'lib/vendor/jackalope/lib/vendor/doctrine-dbal',

Define a mapping driver

You can choose between the drivers for annotations, xml and yml configuration files:

<?php
// Annotation driver
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
$driver = new \Doctrine\ODM\PHPCR\Mapping\Driver\AnnotationDriver($reader, array('/path/to/your/document/classes'));

// Xml driver
$driver = new \Doctrine\ODM\PHPCR\Mapping\Driver\XmlDriver(array('/path/to/your/mapping/files'));

// Yaml driver
$driver = new \Doctrine\ODM\PHPCR\Mapping\Driver\YamlDriver(array('/path/to/your/mapping/files'));

Bootstrap the PHPCR session

With the Jackrabbit provider, the PHPCR ODM connection can be configured with:

<?php
$repository = \Jackalope\RepositoryFactoryJackrabbit::getRepository(
                    array('jackalope.jackrabbit_uri' => 'http://localhost:8080/server'));
$credentials = new \PHPCR\SimpleCredentials('user', 'pass');
$session = $repository->login($credentials, 'your_workspace');

With Midgard2, the connection configuration (using MySQL as an example) would be something like:

<?php
$repository = \Midgard\PHPCR\RepositoryFactory::getRepository(
    array(
        'midgard2.configuration.db.type' => 'MySQL',
        'midgard2.configuration.db.name' => 'phpcr',
        'midgard2.configuration.db.host' => 'localhost',
        'midgard2.configuration.db.username' => 'midgard',
        'midgard2.configuration.db.password' => 'midgard',
        'midgard2.configuration.blobdir' => '/some/path/for/blobs',
        'midgard2.configuration.db.init' => true
    )
);
$credentials = new \PHPCR\SimpleCredentials('admin', 'password');
$session = $repository->login($credentials, 'your_workspace');

Note that the midgard2.configuration.db.init setting should only be used the first time you connect to the Midgard2 repository. After that the database is ready and this setting should be removed for better performance.

Initialize the DocumentManager

<?php
$config = new \Doctrine\ODM\PHPCR\Configuration();
$config->setMetadataDriverImpl($driver);

$dm = new \Doctrine\ODM\PHPCR\DocumentManager($session, $config);

Now you are ready to use the PHPCR ODM

Example usage

<?php
// fetching a document by JCR path (id in PHPCR ODM lingo)
$user = $dm->getRepository('Namespace\For\Document\User')->find('/bob');
//or let the odm find the document class for you
$user = $dm->find('/bob');

// create a new document
$newUser = new \Namespace\For\Document\User();
$newUser->username = 'Timmy';
$newUser->email = 'foo@example.com';
$newUser->path = '/timmy';
// make the document manager know this document
// this will create the node in phpcr but not read the fields or commit
// the changes yet.
$dm->persist($newUser);

// store all changes, insertions, etc. with the storage backend
$dm->flush();

// run a query
$query = $dm->createQuery('SELECT *
                    FROM [nt:unstructured]
                    WHERE ISCHILDNODE("/functional")
                    ORDER BY username',
                    \PHPCR\Query\QueryInterface::JCR_SQL2);
$query->setLimit(2);
$result = $this->dm->getDocumentsByQuery($query, 'My\Document\Class');
foreach ($result as $document) {
    echo $document->getId();
}
// remove a document - and all documents in paths under that one!
$dm->remove($newUser);
$dm->flush();

Document Classes

You write your own document classes that will be mapped to and from the phpcr database by doctrine. The documents are usually simple

<?php
namespace Acme\SampleBundle\Document;

use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;

/**
 * @PHPCRODM\Document
 */
class MyDocument
{
    /**
     * @PHPCRODM\Id()
     */
    public $path;
    /**
     * @PHPCRODM\String()
     */
    public $title;

    /**
     * @PHPCRODM\String()
     */
    public $content;
}

Note that there are basic Document classes for the standard PHPCR node types nt:file, nt:folder and nt:resource See lib/Doctrine/ODM/PHPCR/Document/

Storing documents in the repository: Id Generator Strategy

When defining an id its possible to choose the generator strategy. The id is the path where in the phpcr content repository the document should be stored. By default the assigned id generator is used, which requires manual assignment of the path to a field annotated as being the Id. You can tell doctrine to use a different strategy to find the id.

A document id can be defined by the Nodename and the ParentDocument annotations. The resulting id will be the id of the parent concatenated with '/' and the Nodename.

If you supply a ParentDocument annotation, the strategy is automatically set to parent. This strategy will check the parent and the name and will fall back to the id field if either is missing.

Currently, there is the "repository" strategy which calls can be used which calls generateId on the repository class to give you full control how you want to build the path.

<?php
namespace Acme\SampleBundle\Document;

use Doctrine\ODM\PHPCR\Id\RepositoryIdInterface;
use Doctrine\ODM\PHPCR\DocumentRepository as BaseDocumentRepository;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;

/**
 * @PHPCRODM\Document(repositoryClass="Acme\SampleBundle\DocumentRepository")
 */
class Document
{
    /** @PHPCRODM\Id(strategy="repository") */
    public $id;
    /** @PHPCRODM\String(name="title") */
    public $title;
}

class DocumentRepository extends BaseDocumentRepository implements RepositoryIdInterface
{
    /**
     * Generate a document id
     *
     * @param object $document
     * @return string
     */
    public function generateId($document)
    {
        return 'functional/'.$document->title;
    }
}

Available annotations

Id: The phpcr path to this node. (see above). For new nodes not using the default strategy, it is populated during the persist() operation.
Uuid: The unique id of this node. (only allowed if node is referenceable).
Version: The version of this node, for versioned nodes.
Node: The PHPCR NodeInterface instance for direct access. This is populated as soon as you register the document with the manager using persist(). (This is subject to be removed when we have mapped all functionality you can get from the PHPCR node.)
Nodename: The name of the PHPCR node (this is the part of the path after the last '/' in the id). This property is read only except on document creation with the parent strategy. For new nodes, it is populated during the persist() operation.
ParentDocument: The parent document of this document. If a type is defined, the document will be of that type, otherwise Doctrine\ODM\PHPCR\Document\Generic will be used. This property is read only except on document creation with the parent strategy.
Child(name=x): Map the child with name x to this field.
Children(filter=x): Map the collection of children with matching name to this field. Filter is optional and works like the parameter in PHPCR Node::getNodes() (see the API)
ReferenceOne(targetDocument="myDocument", weak=false): (*)Refers a document of the type myDocument. The default is a weak reference. By optionaly specifying weak=false you get a hard reference. It is optional to specify the targetDocument, you can reference any document.
ReferenceMany(targetDocument="myDocument", weak=false): (*)Same as ReferenceOne except that you can refer many documents with the same document and reference type. If you dont't specify targetDocument you can reference different documents with one property.
Referrers(filter="x", referenceType=null): A field of this type stores documents that refer this document. filter is optional. Its value is passed to the name parameter of Node::getReferences() or Node::getWeakReferences(). You can also specify an optional referenceType, weak or hard, to only get documents that have either a weak or a hard reference to this document. If you specify null then all documents with weak or hard references are fetched, which is also the default behavior.
LocaleIndentifies the field that will be used to store the current locale of the document. This annotation is required for translatable documents.
String,
Binary,
Long (alias Int),
Decimal,
Double (alias Float),
Date,
Boolean,
Name,
Path,
Uri
Map node properties to the document. See PHPCR\PropertyType for details about the types.

(*) Note that creating new references with the help of the ReferenceOne/ReferenceMany annotations is only possible if your PHPCR implementation supports programmatically setting the uuid property at node creation.

Parameters for the property types

In the parenthesis after the type, you can specify some additional information like the name of the PHPCR property to store the value in.

nameThe property name to use for storing this field. If not specified, defaults to the php variable name.
multivalueSet multivalue=true to mark this property as multivalue. It then contains an array of values instead of just one value. For more complex data structures, use child nodes.
translatedSet translated=true to mark this property as being translated. See below.
<?php

/**
 * @PHPCRODM\String(name="categories", multivalue=true)
 */
private $cat;

Multilingual documents

PHPCR-ODM supports multilingual documents so that you can mark properties as translatable and then make the document manager automatically store the translations.

To use translatable documents you need to use several annotations and some bootstrapping code.

<?php
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;

/**
 * @PHPCRODM\Document(alias="translation_article", translator="attribute")
 */
class Article
{
    /** @PHPCRODM\Id */
    public $id;

    /**
     * The language this document currently is in
     * @PHPCRODM\Locale
     */
    public $locale = 'en';

    /**
     * Untranslated property
     * @PHPCRODM\Date
     */
    public $publishDate;

    /**
     * Translated property
     * @PHPCRODM\String(translated=true)
     */
    public $topic;

    /**
     * Language specific image
     * @PHPCRODM\Binary(translated=true)
     */
    public $image;
}

Note that translation always happens on a document level, not on individual fields. With the above document, there is no way to store a new translation for the topic without generating a copy of the image (unless you remove the translated=true from image, but then the image is no longer translated for any language).

Select the translation strategy

A translation strategy needs to be selected by adding the translator parameter to the @Document annotation. The translation strategy is responsible to actually persist the translated properties.

There are two default translation strategies implemented:

  • attribute - will store the translations in attributes of the node containing the translatable properties
  • child - will store the translations in a child node of the node containing the translatable properties

It is possible to implement other strategies to persist the translations, see below.

Implementing your own translation strategy

You may want to implement your own translation strategy to persist the translatable properties of a node. For example if you want all the translations to be stored in a separate branch of you content repository.

To do so you need to implement the Doctrine\ODM\PHPCR\Translation\TranslationStrategy\TranslationStrategyInterface.

Then you have to register your translation strategy with the document manager during the bootstrap.

<?php
class MyTranslationStrategy implements Doctrine\ODM\PHPCR\Translation\TranslationStrategy\TranslationStrategyInterface
{
    // ...
}

$dm = new \Doctrine\ODM\PHPCR\DocumentManager($session, $config);
$dm->setTranslationStrategy('my_strategy_name', new MyTranslationStrategy());

After registering your new translation strategy you can use it in the @Document annotation:

<?php
/**
 * @PHPCRODM\Document(alias="translation_article", translator="my_strategy_name")
 */
class Article
{
    // ...
}

Select the language chooser strategy

The language chooser strategy provides the default language and a list of languages to be used as language fallback order to find the best available translation.

On reading, PHPCR-ODM tries to find a translation with each of the languages in that list and throws a not found exception if none of the languages exists.

The default language chooser strategy (Doctrine\ODM\PHPCR\Translation\LocaleChooser\LocaleChooser) returns a configurable list of languages based on the requested language. On instantiation, you specify the default locale. This can be hardcoded or based on the request or whatever you chose.

When you bootstrap the document manager, you need to set the language chooser strategy if you have any translatable documents:

<?php
$localePrefs = array(
    'en' => array('en', 'de', 'fr'), // When EN is requested try to get a translation first in EN, then DE and finally FR
    'fr' => array('fr', 'de', 'en'), // When FR is requested try to get a translation first in FR, then DE and finally EN
    'it' => array('fr', 'de', 'en'), // When IT is requested try to get a translation first in FR, then DE and finally EN
);

$dm = new \Doctrine\ODM\PHPCR\DocumentManager($session, $config);
$dm->setLocaleChooserStrategy(new LocaleChooser($localePrefs, 'en'));

You can write your own strategy by implementing Doctrine\ODM\PHPCR\Translation\LocaleChooser\LocaleChooserInterface. This is useful to determine the default language based on some logic, or provide fallback orders based on user preferences.

Mark a field as @Locale

All the translatable documents (i.e. having at least one translatable field) must define a field that will hold the current locale of the node. This is done with the @Locale annotation. You may set a default value.

<?php
/**
 * @PHPCRODM\Locale
 */
public $locale = 'en';

This field is mandatory and is not persisted to the content repository.

Setting properties as translatable

A property is set as translatable adding the translatable parameter to the field definition annontation.

<?php
/** @PHPCRODM\String(translated=true) */
public $topic;

You can set any type of property as translatable.

Having at least one property marked as translatable will make the whole document translatable and thus forces you to have a @Locale field (see above).

Please note that internally, the translatable properties will be persisted by the translator strategy, not directly by the document manager.

Translations and references / hierarchy

For now, Child, Children, Parent, ReferenceMany, ReferenceOne and Referrers will all fall back to the default language. The reason for this is that there can be only one tracked instance of a document per session. (Otherwise what should happen if both copies where modified?...).

For more details, see the wiki page and the TODO at the top if this README.

Translation API

Please refer to the phpDoc of the following functions:

For reading:

  • DocumentManager::find (uses the default locale)
  • DocumentManager::findTranslation (allows you to specify which locale to load)
  • DocumentManager::getLocalesFor (get the available locales of a document)

For writing:

  • DocumentManager::persist (save document in language based on @Locale or default language)
  • DocumentManager::persitTranslation (save document with explicit language context)

Example

<?php

// bootstrap the DocumentManager as required (see above)

$localePrefs = array(
    'en' => array('en', 'fr'),
    'fr' => array('fr', 'en'),
);

$dm = new \Doctrine\ODM\PHPCR\DocumentManager($session, $config);
$dm->setLocaleChooserStrategy(new LocaleChooser($localePrefs, 'en'));

// then to use translations:

$doc = new Article();
$doc->id = '/my_test_node';
$doc->author = 'John Doe';
$doc->topic = 'An interesting subject';
$doc->text = 'Lorem ipsum...';

// Persist the document in English
$this->dm->persistTranslation($this->doc, 'en');

// Change the content and persist the document in French
$this->doc->topic = 'Un sujet intéressant';
$this->dm->persistTranslation($this->doc, 'fr');

// Flush to write the changes to the phpcr backend
$this->dm->flush();

// Get the document in default language (English if you bootstrapped as in the example)
$doc = $this->dm->find('Doctrine\Tests\Models\Translation\Article', '/my_test_node');

// Get the document in French
$doc = $this->dm->find('Doctrine\Tests\Models\Translation\Article', '/my_test_node', 'fr');

Lifecycle callbacks

You can use @PHPCRODM\PostLoad and friends to have doctrine call a method without parameters on your entity.

You can also define event listeners on the DocumentManager with $dm->getEventManager()->addEventListener(array(<events>), listenerclass); Your class needs event name methods for the events. They get a parameter of type Doctrine\Common\EventArgs. See also http://www.doctrine-project.org/docs/orm/2.0/en/reference/events.html

  • preRemove - occurs before a document is removed from the storage
  • postRemove - occurs after the document has been successfully removed
  • prePersist - occurs before a new document is created in storage
  • postPersist - occurs after a document has been created in storage. generated ids will be available in this state.
  • preUpdate - occurs before an existing document is updated in storage, during the flush operation
  • postUpdate - occurs after an existing document has successfully been updated in storage
  • postLoad - occurs after the document has been loaded from storage