doctrine/phpcr-odm
Doctrine PHPCR-ODM maps PHP objects to a PHPCR content repository (e.g., Jackrabbit or DBAL-backed implementations). Provides persistence, querying, and schema mapping via Doctrine-style metadata, with tooling, docs, and test setups for multiple backends.
Install the Package:
composer require doctrine/phpcr-odm
Choose a PHPCR implementation (e.g., Jackrabbit or Doctrine DBAL):
composer require jackalope/jackalope-jackrabbit # or jackalope/jackalope-doctrine-dbal
Configure the ODM:
Create a config/packages/doctrine_phpcr_odm.yaml (Symfony) or config/odm.php (Laravel):
doctrine_phpcr_odm:
connections:
default:
url: 'http://localhost:8080/server' # Jackrabbit example
# OR for DBAL:
# url: 'mysql://user:pass@localhost/db'
manager_type: 'document'
backend_options:
# PHPCR-specific config (e.g., Jackrabbit workspace)
Define a Document: Use PHP attributes (recommended) or XML/YAML:
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
#[PHPCR\Document]
class Article {
#[PHPCR\Id]
private string $id;
#[PHPCR\String]
private string $title;
#[PHPCR\ReferenceOne(targetDocument: Article::class)]
private ?Article $parent = null;
#[PHPCR\Children]
private Collection $children;
}
First Query:
$dm = $container->get('doctrine_phpcr.odm.default_document_manager');
$articles = $dm->createQueryBuilder('Article')
->where('title LIKE :title')
->setParameter('title', '%Laravel%')
->getQuery()
->execute();
CRUD with Hierarchy:
// Create a nested document
$parent = $dm->find(Article::class, '/articles/parent');
$child = new Article();
$child->setTitle('Child Article');
$parent->addChild($child);
$dm->persist($child);
$dm->flush();
// Traverse hierarchy
foreach ($parent->getChildren() as $child) {
echo $child->getTitle();
}
Querying Hierarchies:
// Path-based query
$qb = $dm->createQueryBuilder('Article')
->where('jcr:path LIKE /articles/%')
->getQuery();
// Ancestor/descendant queries
$qb = $dm->createQueryBuilder('Article')
->where('jcr:ancestor-node(/articles/parent)')
->getQuery();
Lifecycle Events:
// Pre-persist hook
$dm->getEventManager()->addEventListener(
'prePersist',
function (LifecycleEventArgs $args) {
$doc = $args->getObject();
if ($doc instanceof Article) {
$doc->setCreatedAt(new \DateTime());
}
}
);
Bulk Operations:
// Batch updates
$dm->createQueryBuilder('Article')
->update()
->set('title', 'Updated Title')
->where('jcr:path LIKE /articles/%')
->getQuery()
->execute();
doctrine_phpcr_odm bundle for autoconfiguration.DocumentManager to the container and use phpcr-odm facade:
$this->app->bind('phpcr-odm', function ($app) {
return $app->make('doctrine_phpcr.odm.default_document_manager');
});
doctrine/phpcr-migrations for schema changes:
composer require doctrine/phpcr-migrations
php bin/console doctrine:phpcr:migrations:diff
Session Management:
$dm->flush() explicitly for session commits.Path Conflicts:
$doc->setPath('/custom/path')) can break hierarchy integrity.ReferenceOne/Children for parent-child relationships instead of raw paths.Query Limitations:
JOIN syntax. Use jcr:path or jcr:ancestor-node() for hierarchical queries.->fetch('children').Attribute Migration:
@PHPCR\Field) are deprecated. Use #[PHPCR\Field] instead.php bin/console make:odm-migration to update mappings.Transaction Isolation:
$dm->beginTransaction();
try {
$dm->persist($doc);
$dm->flush();
$dm->commit();
} catch (\Exception $e) {
$dm->rollback();
throw $e;
}
# config/packages/monolog.yaml
monolog:
handlers:
phpcr:
type: stream
path: "%kernel.logs_dir%/phpcr.log"
level: debug
$dm->getSession()->save();
$dm->getSession()->refresh();
$dm->getPath($doc) to debug node locations during traversal.Custom ID Generators:
#[PHPCR\Id(generator: 'uuid')]
private string $id;
Register a custom generator via Configuration::addCustomIdGenerator().
Event Subscribers:
$dm->getEventManager()->addEventSubscriber(
new class implements EventSubscriber {
public function getSubscribedEvents() {
return ['prePersist', 'postRemove'];
}
// ...
}
);
Query DSL Extensions:
Extend Doctrine\ODM\PHPCR\Query\Expression\Comparison for custom predicates:
class CustomComparison extends Comparison {
public function __construct($field, $operator, $value) {
parent::__construct($field, $operator, $value);
}
public function compile(Compiler $compiler) {
$compiler->expr('custom_predicate(' . $this->field . ', ' . $this->value . ')');
}
}
Backend-Specific Tweaks:
backend_options.jcr:path indexing for large trees.jcr:level() or jcr:depth() limits.$children = $parent->getChildren()->matching($dm->createQueryBuilder('Article')->getQuery());
How can I help you explore Laravel packages today?