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 the Package:
composer require doctrine/phpcr-odm jackalope/jackalope-doctrine-dbal
(Use jackalope/jackalope-jackrabbit for Jackrabbit backend if needed.)
Configure PHPCR Backend (e.g., Doctrine DBAL):
// config/phpcr.php
return [
'connections' => [
'default' => [
'driver' => 'jackalope/doctrine-dbal',
'db' => [
'driver' => 'pdo_mysql',
'host' => env('DB_HOST'),
'dbname' => env('DB_DATABASE'),
'user' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
],
'repository_path' => '/',
],
],
];
Create a Service Provider (app/Providers/PHPCRServiceProvider.php):
use Doctrine\ODM\PHPCR\Configuration;
use Doctrine\ODM\PHPCR\DocumentManager;
use Doctrine\ODM\PHPCR\DocumentManagerFactory;
class PHPCRServiceProvider extends ServiceProvider {
public function register() {
$config = new Configuration();
$config->setMetadataDirectory(__DIR__.'/../resources/odm');
$config->setProxyDir(__DIR__.'/../storage/framework/proxies');
$config->setProxyNamespace('Proxies');
$conn = $this->app['config']['phpcr.connections.default'];
$config->setRepositoryImplementation($conn['driver']);
$config->setRepositoryConfig($conn);
$dmFactory = DocumentManagerFactory::create($config);
$this->app->singleton(DocumentManager::class, function () use ($dmFactory) {
return $dmFactory->createDocumentManager();
});
}
}
Define a Document Model (app/Models/Page.php):
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
#[PHPCR\Document]
class Page {
#[PHPCR\Id]
private $id;
#[PHPCR\String]
private $title;
#[PHPCR\ReferenceMany(targetDocument="Page")]
private $children;
public function __construct() {
$this->children = new \Doctrine\Common\Collections\ArrayCollection();
}
}
First Usage (e.g., in a controller):
use Doctrine\ODM\PHPCR\DocumentManager;
public function createPage(DocumentManager $dm) {
$page = new Page();
$page->setTitle('Home');
$dm->persist($page);
$dm->flush();
}
Hierarchical Content Creation:
// Create a blog post with categories (parent-child relationship)
$blog = new Page();
$blog->setTitle('Blog');
$dm->persist($blog);
$post = new Page();
$post->setTitle('First Post');
$post->setParentDocument($blog); // Sets hierarchy
$dm->persist($post);
$dm->flush();
Persist/Flush:
$dm->persist($document); // Marks as "new"
$dm->flush(); // Saves to PHPCR
Merge (Deprecated in 2.0+):
Use find() + manual updates instead of merge().
Detach:
$dm->detach($document); // Removes from DocumentManager
Find by Path:
$dm->find(null, '/site/blog/posts'); // Returns all posts under /site/blog/posts
Subtree Queries:
$query = $dm->createQueryBuilder('Page')
->where('jcr:path LIKE /site/%')
->getQuery();
Parent-Child Relationships:
$parent = $dm->find(Page::class, '/site/home');
$children = $parent->getChildren(); // ChildrenCollection
Pre-Persist:
$dm->getEventManager()->addEventListener(
'prePersist',
function ($event) {
$document = $event->getDocument();
if ($document instanceof Page) {
$document->setCreatedAt(new \DateTime());
}
}
);
Post-Flush:
$dm->getEventManager()->addEventListener(
'postFlush',
function () {
// Log or cache after flush
}
);
Batch Persist:
$pages = [];
foreach ($rawData as $data) {
$pages[] = new Page($data);
}
$dm->persistAll($pages);
$dm->flush();
Delete Subtree:
$dm->remove($parentDocument); // Deletes parent + all children
$dm->flush();
Service Container Binding:
$this->app->bind('phpcr.dm', function () {
return $this->app->make(DocumentManager::class);
});
Eloquent Coexistence:
// Hybrid repository example
$user = User::find(1); // Eloquent
$page = $dm->find(Page::class, '/home'); // PHPCR ODM
Caching:
use Symfony\Contracts\Cache\CacheInterface;
$cache = $this->app->make(CacheInterface::class);
$dm->getConfiguration()->setMetadataCacheImpl($cache);
UUID:
#[PHPCR\Id(generator="uuid")]
private $id;
Path-Based:
#[PHPCR\Id(strategy="path")]
private $id;
| Type | Annotation Example | Use Case |
|---|---|---|
| String | @String |
Titles, descriptions |
| Binary | @Binary |
File uploads |
| DateTime | @DateTime |
Publish dates |
| ReferenceOne | @ReferenceOne(targetDocument="Page") |
Parent-child links |
| ReferenceMany | @ReferenceMany(targetDocument="Page") |
Children collections |
| Boolean | @Boolean |
Published flag |
| Float | @Float |
Ratings |
Path Collisions:
Avoid manually setting jcr:path unless necessary. Let PHPCR generate paths automatically.
Fix: Use @Id(strategy="path") with caution.
Circular References:
Bidirectional @ReferenceOne/@ReferenceMany can cause infinite loops.
Fix: Use lazy loading or @PHPCR\Lazy.
Transaction Isolation: PHPCR and Eloquent transactions may not auto-commit together. Fix: Wrap in explicit transactions:
DB::transaction(function () use ($dm) {
$dm->flush();
});
Case Sensitivity:
Node paths are case-sensitive in PHPCR.
Fix: Normalize paths (e.g., /site/Blog vs /site/blog).
Large Subtrees:
Querying deep hierarchies (e.g., /site/*/*/*) can be slow.
Fix: Use jcr:level or limit depth:
$qb->where('jcr:level < 5');
Enable PHPCR Logging:
$dm->getConfiguration()->setSQLLogger(new \Doctrine\ODM\PHPCR\Logging\SQLLogger());
Check Node Existence:
if (!$dm->find(null, '/nonexistent')) {
throw new \RuntimeException('Node not found');
}
Clear Metadata Cache:
php artisan cache:clear
(If using Symfony Cache.)
PSR-6 Cache Requirement:
Ensure your cache driver implements Psr\Cache\CacheInterface.
Fix: Use symfony/cache or predis/predis:
$cache = new \Symfony\Contracts\Cache\ArrayCache();
$config->setMetadataCacheImpl($cache);
Jackrabbit Java Dependency: If using Jackrabbit, ensure:
9090 is free (default Jackrabbit port).How can I help you explore Laravel packages today?