Migration zum neuen Doctrine-basierten Content Storage
Nach drei Jahren Entwicklungszeit hat Sulu am 26. November 2025 die Version 3.0.0 veröffentlicht. Das größte Update seit dem initialen Release ersetzt das PHPCR-basierte Content-Storage komplett durch ein modernes, Doctrine-ORM-basiertes System. Die Benchmark-Ergebnisse sprechen für sich: doppelter Durchsatz bei halbierter Latenz. Für bestehende Sulu-Projekte bedeutet das allerdings eine signifikante Migration – dieser Artikel zeigt den kompletten Upgrade-Pfad inklusive aller technischen Fallstricke.
PHPCR: Warum das alte System an seine Grenzen stieß
Das PHP Content Repository (PHPCR) war bei der Entwicklung von Sulu 1.x eine sinnvolle Wahl. Es bot viele Features out-of-the-box und ermöglichte schnelle Entwicklung ohne eigene Storage-Implementierung. In der Praxis zeigten sich jedoch zunehmend Limitierungen, die das Sulu-Team zur grundlegenden Neukonzeption zwangen.
Die technischen Probleme von PHPCR
PHPCR skalierte schlecht bei wachsenden Content-Mengen. Features wie Versionierung mussten nachträglich "angeflanscht" werden, statt nativ integriert zu sein. Die proprietäre Query-Sprache erschwerte die Arbeit für Entwickler, die mit SQL oder DQL vertraut waren. Dazu kam die Abhängigkeit von Apache Jackrabbit als Backend – ein zusätzlicher Java-Prozess, der Infrastruktur-Overhead erzeugte.
# PHPCR-Abhängigkeit in Sulu 2.x
# Jackrabbit musste separat laufen:
java -jar jackrabbit-standalone-2.20.0.jar
# Oder über Doctrine DBAL:
# config/packages/sulu_document_manager.yaml
sulu_document_manager:
phpcr:
backend: dbalFür Entwickler war PHPCR oft eine "Black Box" – die meisten Sulu-Nutzer verstanden das System nicht wirklich und arbeiteten drumherum statt damit. Bei manchen potenziellen Nutzern war PHPCR sogar ein Grund, Sulu nicht einzusetzen.
Das neue Content-Storage-System in Sulu 3.0
Das Sulu-Team definierte klare Anforderungen für das neue Storage-System:
- Fine-grained Loading: Kein Bulk-Fetching unnötiger Inhalte mehr
- Native Features: Drafting und Versioning direkt integriert, nicht nachgerüstet
- Flexible Architektur: Jede Page oder Article ist eine Doctrine-ORM-Entity, erweiterbar nach Bedarf
- Standard Query Language: Content über normale SQL- oder DQL-Queries abrufbar
Nach Evaluierung von Alternativen wie Neos entschied sich das Team für eine Eigenentwicklung auf Basis von Doctrine ORM.
Die neue multidimensionale Content-Architektur
Das neue System führt ein multidimensionales Page-Model ein, das Versionierung, Workflow-Stages und Sprachen elegant handhabt:
// Sulu 3.0: Neue Entity-Struktur
namespace App\Entity;
use Sulu\Bundle\ContentBundle\Content\Domain\Model\ContentRichEntityInterface;
use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface;
class Page implements ContentRichEntityInterface
{
private ?string $id = null;
// Dimension Contents: Locale + Stage (draft/live) + Version
private Collection $dimensionContents;
public function getDimensionContents(): Collection
{
return $this->dimensionContents;
}
public function createDimensionContent(): DimensionContentInterface
{
return new PageDimensionContent($this);
}
}
// DimensionContent hält die tatsächlichen Inhalte
class PageDimensionContent implements DimensionContentInterface
{
private string $locale;
private string $stage; // 'draft' oder 'live'
private ?int $version = null;
// SEO-Daten jetzt als JSON-Feld
private array $seoData = [];
// Excerpt-Daten ebenfalls JSON
private array $excerptData = [];
// Template-Daten
private array $templateData = [];
}Performance-Benchmark: PHPCR vs. Doctrine
Die Sulu-Entwickler dokumentieren signifikante Performance-Verbesserungen:
- Durchsatz: 2x höher als mit PHPCR
- Latenz: 50% reduziert gegenüber PHPCR
- Keine Java-Abhängigkeit: Jackrabbit entfällt komplett
- Standard-Tools: MySQL/PostgreSQL Debugging mit gewohnten Tools
# Sulu 2.x mit PHPCR (Jackrabbit Backend)
# Durchschnittliche Page-Load Zeit: 45ms
# Memory Peak: 32MB
# Sulu 3.0 mit Doctrine ORM
# Durchschnittliche Page-Load Zeit: 22ms
# Memory Peak: 18MB
# Gemessen mit 500 Pages, 3 Locales, Draft+LiveSEAL: Die neue Suchintegration
Sulu 3.0 ersetzt das alte MassiveSearchBundle durch SEAL (Search Engine Abstraction Layer). SEAL ist vergleichbar mit Flysystem für File Storage oder Doctrine DBAL für Datenbanken – eine Abstraktionsschicht über verschiedene Suchengines.
Unterstützte Search-Backends
SEAL unterstützt folgende Suchengines:
- Elasticsearch – Der Klassiker für Enterprise-Setups
- OpenSearch – Open-Source-Alternative zu Elasticsearch
- Meilisearch – Schneller, ressourcenschonender Newcomer
- Algolia – Managed Service für Teams ohne Infrastruktur-Kapazitäten
- Redisearch – Für Redis-basierte Stacks
- Solr – Legacy-Kompatibilität
- Loupe – Leichtgewichtige PHP-native Lösung
- Typesense – Open-Source-Alternative zu Algolia
# config/packages/seal.yaml
seal:
engines:
default:
adapter: meilisearch://127.0.0.1:7700
# Oder für Elasticsearch:
engines:
default:
adapter: elasticsearch://127.0.0.1:9200
# Sulu-spezifische SEAL-Konfiguration
# config/packages/sulu_search.yaml
sulu_search:
website:
indexes:
pages: pages_#webspace#_published
articles: articles_#webspace#_publishedWebsite-Search-Controller
Sulu 3.0 liefert neue Website-Search-Controller mit vollständiger SEAL-Integration:
// Neuer WebsiteSearchController in Sulu 3.0
namespace Sulu\Bundle\SearchBundle\Controller;
use Sulu\Bundle\SearchBundle\Search\SearchManagerInterface;
class WebsiteSearchController
{
public function __construct(
private SearchManagerInterface $searchManager
) {}
public function searchAction(Request $request): Response
{
$query = $request->query->get('q', '');
$locale = $request->getLocale();
$webspaceKey = $this->requestAnalyzer->getWebspace()->getKey();
// SEAL-basierte Suche
$results = $this->searchManager->search(
$query,
locale: $locale,
indexes: ['pages_' . $webspaceKey . '_published']
);
return $this->render('search/results.html.twig', [
'results' => $results,
'query' => $query
]);
}
}Migration von Sulu 2.x zu 3.0: Der komplette Pfad
Die Migration erfordert mehrere koordinierte Schritte. Das SuluPHPCRMigrationBundle übernimmt die Datenmigration, aber Skeleton-Anpassungen und Code-Updates müssen manuell erfolgen.
Phase 1: Voraussetzungen prüfen
# Aktuelle Version verifizieren
composer show sulu/sulu
# PHP-Version prüfen (mindestens 8.2 erforderlich)
php -v
# Symfony-Version prüfen (5.4+ oder 7.0+)
composer show symfony/framework-bundle
# Backup erstellen (kritisch!)
mysqldump -u root -p sulu_db > sulu_backup_pre_migration.sql
# Jackrabbit-Workspace sichern (falls verwendet)
cp -r /path/to/jackrabbit/workspaces /backup/Phase 2: Composer-Dependencies aktualisieren
# Sulu 3.0 Requirements
composer require sulu/sulu:^3.0
# Migration Bundle installieren
composer require sulu/phpcr-migration-bundle
# Bundles registrieren
# config/bundles.php
return [
// Bestehende Bundles...
Sulu\Bundle\PhpcrMigrationBundle\SuluPhpcrMigrationBundle::class => ['all' => true],
];Phase 3: Migration-Bundle konfigurieren
# config/packages/sulu_phpcr_migration.yaml
sulu_phpcr_migration:
# Für Jackrabbit-Backend:
# DSN: "jackrabbit://admin:admin@127.0.0.1:8080/server?workspace=%env(PHPCR_WORKSPACE)%"
# Für Doctrine DBAL-Backend (häufigster Fall):
DSN: "dbal://default?workspace=%env(PHPCR_WORKSPACE)%"
target:
dbal:
connection: defaultPhase 4: Datenbankschema vorbereiten
Die neuen Tabellen müssen angelegt werden, bevor die Content-Migration startet:
# Doctrine-Migrationen generieren und ausführen
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
# PHPCR-Struktur für Migration vorbereiten
php bin/adminconsole phpcr:migrations:migratePhase 5: Content-Migration ausführen
# Vollständige Migration aller Document-Typen
php bin/adminconsole sulu:phpcr-migration:migrate
# Oder selektiv nach Document-Typ:
php bin/adminconsole sulu:phpcr-migration:migrate page
php bin/adminconsole sulu:phpcr-migration:migrate article
php bin/adminconsole sulu:phpcr-migration:migrate snippet
# Bei Fehlern: Befehl kann erneut ausgeführt werden
# Bereits migrierte Inhalte werden überschrieben, nicht dupliziertPhase 6: SEO- und Excerpt-Daten migrieren
In Sulu 3.0 werden SEO- und Excerpt-Daten als JSON-Felder gespeichert. Für Projekte, die bereits RC-Versionen nutzten, sind SQL-Migrationen nötig:
-- Pages Table: SEO-Daten Migration
ALTER TABLE pa_page_dimension_contents
ADD COLUMN seoData JSON NOT NULL DEFAULT (JSON_OBJECT());
UPDATE pa_page_dimension_contents
SET seoData = JSON_OBJECT(
'title', seoTitle,
'description', seoDescription,
'keywords', seoKeywords,
'canonicalUrl', seoCanonicalUrl
)
WHERE seoTitle IS NOT NULL
OR seoDescription IS NOT NULL
OR seoKeywords IS NOT NULL
OR seoCanonicalUrl IS NOT NULL;
ALTER TABLE pa_page_dimension_contents
DROP COLUMN seoTitle,
DROP COLUMN seoDescription,
DROP COLUMN seoKeywords,
DROP COLUMN seoCanonicalUrl;
-- Excerpt-Daten analog migrieren
ALTER TABLE pa_page_dimension_contents
ADD COLUMN excerptData JSON NOT NULL DEFAULT (JSON_OBJECT());
UPDATE pa_page_dimension_contents
SET excerptData = JSON_OBJECT(
'title', excerptTitle,
'description', excerptDescription,
'more', excerptMore,
'image', IF(excerptImageId IS NOT NULL,
JSON_OBJECT('id', excerptImageId), NULL),
'icon', IF(excerptIconId IS NOT NULL,
JSON_OBJECT('id', excerptIconId), NULL)
)
WHERE excerptTitle IS NOT NULL
OR excerptDescription IS NOT NULL
OR excerptMore IS NOT NULL
OR excerptImageId IS NOT NULL
OR excerptIconId IS NOT NULL;
-- Foreign Keys entfernen
ALTER TABLE pa_page_dimension_contents
DROP FOREIGN KEY FK_209A42C02F5A5F5D,
DROP FOREIGN KEY FK_209A42C016996F78;
ALTER TABLE pa_page_dimension_contents
DROP COLUMN excerptTitle,
DROP COLUMN excerptDescription,
DROP COLUMN excerptMore,
DROP COLUMN excerptImageId,
DROP COLUMN excerptIconId;Phase 7: Skeleton-Anpassungen
Das Sulu-Skeleton hat sich zwischen 2.6.17 und 3.0.0 erheblich geändert. Die wichtigsten Anpassungen:
# Navigation Twig-Funktion geändert
# Alt (Sulu 2.x):
{% set navigation = sulu_navigation_root_tree(request.webspaceKey, 2) %}
# Neu (Sulu 3.0):
{% set navigation = sulu_page_navigation_root_tree(request.webspaceKey, 2) %}
# Routen-Kontext: site → webspace
# Alt:
Route::site
# Neu:
Route::webspace
# Custom URLs Bundle aktivieren
# config/bundles.php
Sulu\Bundle\CustomUrlBundle\SuluCustomUrlBundle::class => ['all' => true],
# APP_SHARE_DIR definieren (ersetzt Sulu Common Dir)
# .env
APP_SHARE_DIR=%kernel.project_dir%/var/sharePhase 8: Search-Reindexierung
# Nach erfolgreicher Migration: SEAL-Index aufbauen
php bin/console seal:reindex
# Website-spezifischen Index erstellen
php bin/websiteconsole seal:reindex --index=pages_default_publishedNative PHP-Typen: Modernisierter Codebase
Sulu 3.0 führt durchgehend native PHP-Typen ein. Alle Entities und Interfaces wurden aktualisiert:
// Sulu 2.x: Optionale Typen, PHPDoc-basiert
/**
* @param string|null $title
* @return Page
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
// Sulu 3.0: Native Typen
public function setTitle(?string $title): self
{
$this->title = $title;
return $this;
}
// Betroffene Bundles mit nativen Typen:
// - MediaBundle
// - SecurityBundle
// - CategoryBundle
// - ContactBundle
// - TrashBundle
// - AudienceTargetingBundle
// - ReferenceBundle
// - PreviewBundle
// - WebsiteBundle
// - ActivityBundle
// - TagBundleFür Custom Code bedeutet das: Überprüfen Sie alle Klassen, die Sulu-Interfaces implementieren oder Sulu-Klassen erweitern. Return-Type- und Parameter-Typen müssen exakt übereinstimmen.
Entfernte Features und Breaking Changes
Sulu 3.0 räumt auf – viele deprecated Features sind entfernt:
Entfernte Components
- Doctrine Cache: Ersetzt durch Symfony Cache
- Preview Cache (deprecated): Neues Caching-System
- Object Format für Entity Selections: Nur noch Array-Format
- Media Title Generation (deprecated): Neue Implementierung
- TaggedServiceCollectorCompilerPass: Durch
tagged_iteratorersetzt - SuluKernelBrowser: Nicht mehr benötigt
- ContentTypesCompilerPass: Automatische Service-Registrierung
- ImageTransformationCompilerPass: Neue Konfiguration
- Segments in PortalInformation: Entfernt
Geänderte APIs
// CollectionController: Parameter 'collectionClass' jetzt required
// Alt:
public function cgetAction(Request $request, $collectionClass = null)
// Neu:
public function cgetAction(Request $request, string $collectionClass)
// DimensionContentCollection: Array-Support entfernt
// Alt:
$collection['draft'] // Array-Access
// Neu:
$collection->getDimensionContent('draft') // Method Call
// Search Index ID Separator geändert
// Alt: "::"
// Neu: "__"Migrations-Aufwand und Zeitplanung
Basierend auf der Projektgröße und Custom-Code-Umfang variiert der Migrations-Aufwand erheblich:
Vanilla Sulu-Projekte (minimale Anpassungen)
- Aufwand: 16-24 Stunden
- Risiko: Gering
- Hauptarbeit: Skeleton-Anpassungen, Testing
Projekte mit Custom Document-Typen
- Aufwand: 40-80 Stunden
- Risiko: Mittel
- Hauptarbeit: Custom Migration-Skripte, Entity-Anpassungen
Enterprise-Projekte mit komplexer PHPCR-Logik
- Aufwand: 80-160 Stunden
- Risiko: Hoch
- Hauptarbeit: Vollständige Storage-Layer-Migration, umfangreiches Testing
# Migrations-Checkliste
□ Backup Datenbank und Jackrabbit-Workspace
□ PHP 8.2+ verfügbar
□ Symfony 5.4+ oder 7.0+
□ Custom Document-Typen inventarisiert
□ PHPCR-spezifischer Code identifiziert
□ Test-Environment aufgesetzt
□ Rollback-Plan dokumentiert
□ Staging-Migration durchgeführt
□ Performance-Baseline gemessen
□ Go-Live-Fenster geplantPHP 8.5 Support und Zukunftssicherheit
Sulu 3.0 ist bereits auf PHP 8.5 getestet und unterstützt damit die kommende PHP-Version von Tag 1. Die CI-Pipeline testet explizit gegen PHP 8.2, 8.3, 8.4 und 8.5.
# composer.json von Sulu 3.0
{
"require": {
"php": "^8.2 || ^8.3 || ^8.4",
"composer-runtime-api": "^2.0",
"cmsig/seal": "^0.12.3",
"cmsig/seal-symfony-bundle": "^0.12.2",
"doctrine/dbal": "^3.6",
"doctrine/orm": "^2.14 || ^3.0"
}
}Mit Doctrine ORM 3.0 Support und Flysystem 3 ist Sulu 3.0 für die nächsten Jahre gut aufgestellt.
Fazit: Jetzt migrieren oder warten?
Die Entscheidung hängt von der Projektsituation ab:
Sofort migrieren, wenn:
- Performance-Probleme mit PHPCR bestehen
- Jackrabbit-Infrastruktur Overhead verursacht
- Versionierung dringend benötigt wird
- Neue Projekte geplant sind (direkt mit 3.0 starten)
- Team-Kapazitäten für gründliche Migration vorhanden sind
Abwarten, wenn:
- Komplexe Custom-PHPCR-Logik existiert
- Sulu 2.6 LTS noch Security-Updates erhält
- Keine akuten Performance-Probleme bestehen
- Migration in ruhigere Projektphase geplant werden kann
Die Performance-Gewinne und der Wegfall der Jackrabbit-Abhängigkeit machen Sulu 3.0 langfristig zur besseren Wahl. Die initiale Migrations-Investition amortisiert sich durch reduzierte Wartungskosten und bessere Developer Experience.
Als Symfony-Entwickler beobachte ich die Entwicklung von Sulu 3.0 mit großem Interesse – besonders die Doctrine-basierte Architektur zeigt, wie moderne Content-Management-Systeme heute gebaut werden sollten.
Quellen und weiterführende Links
- Sulu 3.0.0 Release Notes auf GitHub
- SuluPHPCRMigrationBundle - Migration Guide
- Sulu CMS 3.0 Preview: A Content Storage Quantum Leap
- Offizielle Sulu 3.0 Upgrade-Dokumentation
- SEAL - Search Engine Abstraction Layer
- Sulu auf Packagist
Dennis Schwenker-Sanders | Symfony & PHP Development | d-schwenker.de