You're subscribed!

We've added to our mailing list. You'll receive our latest insights and updates directly in your inbox.

Message sent!

Thank you. We've received your message and will get back to you within 24 hours.

Back to home

Inquiry sent!

Thank you. We will review your project description and get back to you within 1 business day.

Application submitted!

Thank you. Your application for has been received. Our team will review it and get back to you soon.

Home/Blog/PHP & Symfony/Migrating from Sulu 2.6 to 3.0: a step-by-step guide
PHP & Symfony

Migrating from Sulu 2.6 to 3.0: a step-by-step guide

  • 11 min read
Frame 1
Table of contents

A Sulu 2.6 to 3.0 migration is the largest upgrade in the history of this CMS. Content moves from the PHPCR repository into regular Doctrine ORM tables, and configuration, templates and part of your custom code change with it. The content itself is moved by an official tool that has a dry-run mode and can be run repeatedly. The scope of the work is set by everything else: how much project code talks to PHPCR, how current your bundles are, and how much leftover data sits in the repository after years of editing. Below are the two phases from the official documentation with commands in execution order, a list of changes you have to make by hand, and five pitfalls described by the Sulu team and the authors of the migration bundle.

Key takeaways. Sulu 2.6 is a long-term support release and receives security fixes until 4.0 ships, so you can schedule the migration without deadline pressure. Sulu 3.0 requires PHP 8.2–8.5 and Symfony 6.4–7.4, and a project on Symfony 5.4 upgrades Symfony first, while still on 2.6. Pages, articles and snippets are moved by sulu/phpcr-migration-bundle, which has a dry run with a JSON error report and overwrites data on a rerun instead of duplicating it. Template XML, Twig and any code that uses the DocumentManager have to be fixed by hand. When the Sulu team migrated their own site, they ran the whole process five times on a copy of production data before deploying the result.

Do you have to migrate now?

No. With the 3.0 release (26 November 2025) the Sulu team declared 2.6 an LTS (long-term support) release. Security and critical bug fixes are due to continue until Sulu 4.0 arrives, and support has ended only for the 1.6 line. New features, however, now land in 3.x only.

It makes sense to plan the migration now when:

  • a larger development stage is ahead (new content types, a template rebuild, a new frontend) and the same files will be changed anyway,
  • you run Jackrabbit or Elasticsearch only for Sulu and want them off your infrastructure,
  • Symfony developers are joining the team who know Doctrine and would have to learn PHPCR from scratch.

It is better to wait when:

  • the project runs on Symfony 5.4 or older (upgrade Symfony first, as a separate release),
  • you depend on third-party bundles that have no 3.0-compatible version,
  • a large share of your logic writes content through the DocumentManager (imports, syncs with external systems) and there is no budget to rewrite it,
  • the site is in maintenance mode and nothing will change in it over the next year.

Requirements and infrastructure changes

According to the compatibility table in the documentation, Sulu 3.0 runs on PHP 8.2–8.5 and Symfony 6.4–7.4. The 2.6 line supports the same PHP versions and Symfony 5.4 to 7.4, so PHP and Symfony can be raised before the migration. The documentation advises against upgrading PHP, Symfony and Sulu in a single step: with three changes at once it is hard to tell which one broke something.

Packages and services change considerably:

  • Removed: sulu/article-bundle (articles are part of the core), massive/search-bundle together with handcraftedinthealps/zendsearch, and the Jackalope packages. In most projects Apache Jackrabbit can be switched off after the migration.
  • Added: the SEAL search layer with the Loupe adapter, which keeps its index in SQLite and needs the pdo_sqlite extension, plus Flysystem 3 configuration for media files.
  • Optional: Elasticsearch. Articles work without it, and projects that need the previous integration install the separate SuluArticleViewDocumentBundle.

The Sulu team states that Loupe handles an index of up to about 50,000 documents efficiently, and that moving to Meilisearch or Elasticsearch is a configuration change. Building the admin panel with custom extensions requires Node.js 20 or newer.

Phase 1: cleanup on Sulu 2.6

The official upgrade guide from 2.6 to 3.0 splits the work into two phases. The first one happens entirely on the old version. Its goal is a content repository free of leftovers from old templates that could stop the migration tool.

  1. Update sulu/sulu to the latest 2.6.x and the ecosystem bundles (forms, headless, automation, articles) to their latest 2.6-compatible versions.
  2. Run the pending PHPCR migrations.
  3. Run the repository cleanup. It removes properties left behind by template changes.
  4. Check the keys of webspaces, templates and navigation contexts. In 3.0 they may contain only lowercase letters, digits, underscores and hyphens, with a maximum of 31 characters.
  5. Dump the database. This is your way back if the second phase fails.
Phase 1: preparation on Sulu 2.6
composer update sulu/sulu --with-dependencies
php bin/console phpcr:migrations:migrate
php bin/console sulu:document:phpcr-cleanup

After migrating their own site, the Sulu team noted that a clean 2.6 produces less noise in the migration output, which makes it easier to see what really behaves differently.

Phase 2: moving to 3.0 and migrating content

Run the second phase on a separate branch, or better, on a cloned project with its own database. The old installation stays untouched and serves as a reference.

Code comes first. In composer.json you raise sulu/sulu and the sulu/* bundles to 3.0, compare your project with the sulu/skeleton template between the 2.6 and 3.0 branches (configuration, .env, the bundle list) and work through UPGRADE-3.x.md. Some entries require SQL statements to be run by hand. Do not skip them, because later migrations depend on them. Then you run the Doctrine schema migrations, adjust the template XML and rebuild the admin panel.

Only then is it time for content. You install sulu/phpcr-migration-bundle, point it at the source (the database or Jackrabbit) and start a dry run. The tool walks through all content, writes nothing, prints a summary at the end and saves a JSON report with the errors. You fix the template or the data, run it again and repeat until the report is empty. The real migration can be rerun as well: content that was already migrated is overwritten.

Phase 2: schema, content migration, index
php bin/console doctrine:migrations:migrate
composer require sulu/phpcr-migration-bundle
php bin/console sulu:phpcr-migration:migrate --dry-run
php bin/console sulu:phpcr-migration:migrate
php bin/console cmsig:seal:reindex --drop

After the migration, rebuild the search index and review pages, articles, snippets and media in the admin. Then remove the migration bundle. The phpcr_* tables are dropped last, with a separate Doctrine migration generated by doctrine:migrations:diff, once you have confirmed it drops only those tables.

What you have to rewrite by hand

The tool moves data. It does not touch code, so the following changes from the upgrade guide are made by hand:

  • Writing content from PHP. The DocumentManager has been removed. Pages and articles are created and modified by dispatching messages (for example CreatePageMessage or ModifyArticleMessage) through the Symfony message bus. This affects imports, console commands and integrations.
  • Template XML. The address field becomes a route type named url with the sulu.rlp tag, and the title gets the sulu.rlp.part tag. The controller class referenced in the template changes too.
  • Articles. Article types have been replaced by template groups. Each article template gets a <group> element, and user roles have to be granted the new groups in the admin.
  • Twig. sulu_content_load is split into sulu_page_load and sulu_article_load, and the list of properties to load is now a required argument. Navigation functions received the sulu_page_ prefix, and the SEO functions are replaced by the @SuluWebsite/Extension/seo.html.twig template.
  • Smart content. The types and structureTypes parameters give way to groups (for articles) and templateKeys (for pages and snippets). The old names stop the container from building.
  • Headless. In projects with custom controllers based on SuluHeadlessBundle, the signature of resolveHeadlessData() has changed.

If your frontend is a separate application (Nuxt, Next.js, Remix), the Twig changes do not apply to you. Still, compare the API responses field by field before you switch traffic.

Five pitfalls before you call the migration done

The first two were described by the Sulu team after migrating their own site, sulu.io (post dated 7 May 2026). The rest come from the migration bundle's documentation.

1. Errors that only show up with new content

After the sulu.io migration everything looked correct in the admin. The problem surfaced with the first new blog post: its URL lacked the /blog prefix, because the article template was missing the route_schema parameter. Reviewing existing pages will not catch this. After the migration, create and publish one new item of every type.

2. Early 3.0 and bundle versions

During the sulu.io migration, text_area fields holding JSON were decoded incorrectly, and smart content filtering by tag returned all articles instead of the selected ones. Both bugs have been fixed in the migration bundle and in the core. So migrate to the current 3.0.x release with the current bundle version, and skip 3.0.0. Since 3.0.6, DoctrineMigrationsBundle is a required dependency.

3. The dry run does not see everything

According to the bundle's documentation, the dry run catches validation errors (title and URL length, a missing webspace or parent page) and unique-key collisions. It does not detect foreign-key violations, truncation of overlong values or empty required columns. On MySQL there is also a difference in character comparison: values that differ only by an accent, such as Café and Cafe, pass the dry run and then fail in the real migration.

4. An avalanche of errors from one page

If a parent page cannot be migrated, every page below it reports a missing parent. A report with a hundred entries may therefore describe a single error. Fix the first one in the branch and rerun the dry run before reading the rest.

5. Memory on large sites

The dry run keeps written rows in memory until the command ends, roughly 1.6 KB per row. A site with many documents needs a higher memory_limit.

A migration you have run five times on a copy of production comes down to a database import on release day.

How to plan the migration: three rules

Three rules keep the planning in order:

  • Estimate after the dry run. Phase one and a dry run on a copy of production data give you an error report. Together with a list of places in the code that use the DocumentManager, it says more about scope than the number of pages on the site.
  • Two installations side by side. Version 2.6 keeps running until the day of the switch, and the migration can be repeated on a fresh dump as many times as needed. The Sulu team called the parallel setup the best decision of the whole process.
  • Migrate alongside development. A migration is easier to justify in the budget when it accompanies a larger stage of work. Postponed without a plan, it turns into technical debt.

To compare results, the Sulu team used a simple script that crawls the URLs from the sitemap and compares the new installation's responses with a production snapshot.

Summary

A move from Sulu 2.6 to 3.0 is best run as a process to repeat: cleanup on 2.6, code and configuration on 3.0, dry run, fixes, real migration, page comparison. Moving the content is automated. Templates and custom PHPCR-based code need manual work, so start your estimate there. The 2.6 line is supported until 4.0 ships, which lets you fit the date to your site's roadmap.

Running a project on Sulu 2.6 and want to know how much work a migration means for you? Get in touch and tell us about your project.

FAQ

There is no single number. Moving the content takes a few commands, while templates, Twig and custom code that uses PHPCR need manual work, and every project has a different amount of them. A reliable estimate comes from the migration bundle's dry run on a copy of production data, plus a code review for DocumentManager usage.

No. Sulu 2.6 is a long-term support release and receives security and critical bug fixes until Sulu 4.0 is released. New features now appear only in the 3.x line.

The process itself limits the risk: you work on a copy of the project, the 2.6 installation stays untouched, and you dump the database before you start. The migration bundle moves pages, articles and snippets while preserving structure and translations, and a rerun overwrites data instead of duplicating it. You still need to verify the result against real content.

In most projects, yes. The new content storage does not use Jackrabbit, and articles no longer require Elasticsearch. The default search engine, Loupe, runs on SQLite, and Elasticsearch remains one of the options supported by the SEAL layer.

PHP 8.2 to 8.5 and Symfony 6.4 to 7.4. The default search needs the pdo_sqlite extension, and building the admin panel with custom extensions requires Node.js 20 or newer.

It has to be rewritten. The Jackalope classes and the DocumentManager have been removed, and content is written through messages dispatched on the Symfony message bus. Reads go through Doctrine entities, so queries can be written in SQL or DQL.