doctrine/phpcr-odm
Doctrine PHPCR-ODM brings Doctrine-style object document mapping to PHP Content Repository (PHPCR) implementations. Map PHP objects to nodes and query content repositories via familiar Doctrine APIs. Supports Jackrabbit and Doctrine DBAL setups, with tests and docs available.
Install Dependencies
composer require doctrine/phpcr-odm jackalope/jackalope-doctrine-dbal doctrine/dbal
(Use jackalope/jackalope-jackrabbit for Java-based PHPCR if needed.)
Configure PHPCR in Laravel
Create a service provider (e.g., PHPCRServiceProvider) to bootstrap the ODM:
// app/Providers/PHPCRServiceProvider.php
namespace App\Providers;
use Doctrine\ODM\PHPCR\Configuration;
use Doctrine\ODM\PHPCR\DocumentManager;
use Doctrine\ODM\PHPCR\DocumentManagerFactory;
use Doctrine\ODM\PHPCR\Mapping\Driver\AttributeDriver;
use Illuminate\Support\ServiceProvider;
class PHPCRServiceProvider extends ServiceProvider
{
public function register()
{
$config = new Configuration();
$config->setMetadataDriverImpl(new AttributeDriver());
$config->setProxyDir(__DIR__.'/../../storage/framework/proxies');
$config->setProxyNamespace('App\\Proxies');
// Configure PHPCR backend (Doctrine DBAL example)
$config->setRepositoryImplementation('jackalope/jackalope-doctrine-dbal');
$config->setConnectionParams([
'driver' => 'pdo_mysql',
'host' => env('DB_HOST'),
'dbname' => env('DB_DATABASE'),
'user' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
]);
$dm = DocumentManagerFactory::create($config);
$this->app->singleton('phpcr.odm', fn() => $dm);
}
}
Register the provider in config/app.php.
Define a PHPCR Document
// app/Models/Page.php
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
#[PHPCR\Document]
class Page
{
#[PHPCR\Id(strategy: 'uuid')]
private string $id;
#[PHPCR\Field(type: 'string')]
private string $title;
#[PHPCR\Field(type: 'string')]
private string $content;
#[PHPCR\ParentDocument]
private ?Page $parent;
#[PHPCR\Children]
private array $children = [];
// Getters/setters...
}
First Usage in a Controller
use Doctrine\ODM\PHPCR\DocumentManager;
class PageController extends Controller
{
public function __construct(private DocumentManager $dm) {}
public function create()
{
$homePage = new Page();
$homePage->setTitle('Home');
$homePage->setContent('Welcome!');
$this->dm->persist($homePage);
$this->dm->flush();
return redirect()->route('home');
}
}
/home/about/team).$children = $this->dm->createQueryBuilder(Page::class)
->where('parent = :parent')
->setParameter('parent', $parentPage)
->getQuery()
->getResult();
// Create and persist a document
$document = new Page();
$this->dm->persist($document);
$this->dm->flush(); // Saves to PHPCR
// Update and flush
$document->setTitle('Updated Title');
$this->dm->flush();
// Detach (remove from DocumentManager)
$this->dm->detach($document);
// Bulk insert (avoid flush per document)
$documents = [];
for ($i = 0; $i < 100; $i++) {
$doc = new Page();
$doc->setTitle("Page $i");
$documents[] = $doc;
}
$this->dm->persist($documents);
$this->dm->flush(); // Single flush for all
// Set parent-child relationship
$parent = $this->dm->find(Page::class, '/home');
$child = new Page();
$child->setParent($parent);
$this->dm->persist($child);
$this->dm->flush();
// Query children
$children = $parent->getChildren();
// Find nodes under a specific path
$query = $this->dm->createQueryBuilder(Page::class)
->where('path LIKE :path')
->setParameter('path', '/home/%')
->getQuery();
$results = $query->getResult();
// Find by field
$pages = $this->dm->createQueryBuilder(Page::class)
->where('title = :title')
->setParameter('title', 'About Us')
->getQuery()
->getSingleResult();
// Count children recursively
$query = $this->dm->createQueryBuilder(Page::class)
->select('COUNT(c)')
->from(Page::class, 'p')
->leftJoin('p.children', 'c')
->getQuery();
$count = $query->getSingleScalarResult();
// Register a pre-persist listener
$this->dm->getEventManager()->addEventListener(
'prePersist',
fn($event) => {
$document = $event->getDocument();
if ($document instanceof Page) {
$document->setCreatedAt(now());
}
}
);
// Cache query results (PSR-6 compatible)
use Doctrine\Common\Cache\Psr6\Psr6Cache;
$cache = new Psr6Cache($this->app->make(\Psr\Cache\CacheItemPoolInterface::class));
$this->dm->getConfiguration()->setQueryCacheImpl($cache);
// Hybrid repository example
class HybridRepository
{
public function __construct(
private DocumentManager $phpcrDm,
private \Illuminate\Database\Eloquent\Model $eloquentModel
) {}
public function syncData()
{
$phpcrData = $this->phpcrDm->find(Page::class, '/home');
$this->eloquentModel->updateFromPhpcr($phpcrData);
}
}
// Use UUID for IDs
#[PHPCR\Id(strategy: 'uuid')]
private string $id;
// Or custom generator
#[PHPCR\Id(strategy: 'custom')]
#[PHPCR\CustomIdGenerator(class: 'App\Services\CustomIdGenerator')]
private string $id;
Path vs. ID Confusion
/site/pages/home) for hierarchical queries, not just IDs.path in queries for hierarchical data:
// Wrong: Assumes ID is path
$this->dm->find(Page::class, '/home'); // Fails if ID is UUID
// Correct: Use path explicitly
$this->dm->createQueryBuilder(Page::class)
->where('path = :path')
->setParameter('path', '/home')
->getQuery();
Flush Behavior Changes in 2.0+
DocumentManager::flush() no longer saves the PHPCR session if there are no ODM changes.$session->save() if you need to force a session save:
$this->dm->flush(); // ODM changes only
$this->dm->getSession()->save(); // Force session save
ChildrenCollection Quirks
ChildrenCollection::slice() no longer accepts node names as offsets (use sliceByChildName).// Old (deprecated)
$slice = $children->slice('about');
// New
$slice = $children->sliceByChildName('about');
PSR-6 Cache Requirement
$this->app->bind(\Psr\Cache\CacheItemPoolInterface::class, fn() => new \Symfony\Cache\Adapter\FilesystemAdapter());
Attribute Mapping Only
How can I help you explore Laravel packages today?