Sulu 3.0 Migration: Von PHPCR zu Doctrine Content Storage

Sulu 3.0 Migration: Von PHPCR zu Doctrine Content Storage

Sulu 3.0 ist da: Nach dreijähriger Entwicklung ersetzt das neue Release das PHPCR-basierte Content-Storage durch ein modernes Doctrine-ORM-System. Diese fundamentale Neuerung verspricht doppelten Durchsatz bei halbierter Latenz, macht aber eine signifikante Migration für bestehende Projekte notwendig, um alle technischen Fallstricke zu meistern.

Dennis Schwenker-Sanders 9 Min. Lesezeit

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: dbal

Fü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:

  1. Fine-grained Loading: Kein Bulk-Fetching unnötiger Inhalte mehr
  2. Native Features: Drafting und Versioning direkt integriert, nicht nachgerüstet
  3. Flexible Architektur: Jede Page oder Article ist eine Doctrine-ORM-Entity, erweiterbar nach Bedarf
  4. 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:

  1. Durchsatz: 2x höher als mit PHPCR
  2. Latenz: 50% reduziert gegenüber PHPCR
  3. Keine Java-Abhängigkeit: Jackrabbit entfällt komplett
  4. 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+Live

SEAL: 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:

  1. Elasticsearch – Der Klassiker für Enterprise-Setups
  2. OpenSearch – Open-Source-Alternative zu Elasticsearch
  3. Meilisearch – Schneller, ressourcenschonender Newcomer
  4. Algolia – Managed Service für Teams ohne Infrastruktur-Kapazitäten
  5. Redisearch – Für Redis-basierte Stacks
  6. Solr – Legacy-Kompatibilität
  7. Loupe – Leichtgewichtige PHP-native Lösung
  8. 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#_published

Website-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: default

Phase 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:migrate

Phase 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 dupliziert

Phase 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/share

Phase 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_published

Native 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
// - TagBundle

Fü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

  1. Doctrine Cache: Ersetzt durch Symfony Cache
  2. Preview Cache (deprecated): Neues Caching-System
  3. Object Format für Entity Selections: Nur noch Array-Format
  4. Media Title Generation (deprecated): Neue Implementierung
  5. TaggedServiceCollectorCompilerPass: Durch tagged_iterator ersetzt
  6. SuluKernelBrowser: Nicht mehr benötigt
  7. ContentTypesCompilerPass: Automatische Service-Registrierung
  8. ImageTransformationCompilerPass: Neue Konfiguration
  9. 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)

  1. Aufwand: 16-24 Stunden
  2. Risiko: Gering
  3. Hauptarbeit: Skeleton-Anpassungen, Testing

Projekte mit Custom Document-Typen

  1. Aufwand: 40-80 Stunden
  2. Risiko: Mittel
  3. Hauptarbeit: Custom Migration-Skripte, Entity-Anpassungen

Enterprise-Projekte mit komplexer PHPCR-Logik

  1. Aufwand: 80-160 Stunden
  2. Risiko: Hoch
  3. 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 geplant

PHP 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:

  1. Performance-Probleme mit PHPCR bestehen
  2. Jackrabbit-Infrastruktur Overhead verursacht
  3. Versionierung dringend benötigt wird
  4. Neue Projekte geplant sind (direkt mit 3.0 starten)
  5. Team-Kapazitäten für gründliche Migration vorhanden sind

Abwarten, wenn:

  1. Komplexe Custom-PHPCR-Logik existiert
  2. Sulu 2.6 LTS noch Security-Updates erhält
  3. Keine akuten Performance-Probleme bestehen
  4. 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

  1. Sulu 3.0.0 Release Notes auf GitHub
  2. SuluPHPCRMigrationBundle - Migration Guide
  3. Sulu CMS 3.0 Preview: A Content Storage Quantum Leap
  4. Offizielle Sulu 3.0 Upgrade-Dokumentation
  5. SEAL - Search Engine Abstraction Layer
  6. Sulu auf Packagist

Dennis Schwenker-Sanders | Symfony & PHP Development | d-schwenker.de

Artikel teilen: