ezsystems/legacy-bridge
Symfony bundle bridging eZ Publish legacy (4.x) and eZ Platform (5.x), letting legacy extensions, modules, and the legacy kernel run alongside the modern Symfony stack so you can migrate incrementally without a full rewrite.
Read this first: the package is archived (last release Nov 2020). It exists to run the eZ Publish Legacy kernel side-by-side with eZ Platform 2.x, and it is only useful for projects that are already in that migration window. Do not introduce it into a new project, and do not expect it to work on Ibexa 4 / Symfony 5 / PHP 8 — it is pinned to the eZ Platform 2.x + Symfony 3.4/4.x + PHP 7.x era. If you are on eZ Platform 2.x with a legacy install to retire, it is still the sanctioned path.
Minimal steps to a working install:
ezpublish_legacy/ (with settings/, extension/, design/, var/, and its own autoload), matching the DB and settings/override of the platform you are migrating from.composer require ezsystems/legacy-bridge
Symfony 4/Flex: add EzSystems\EzPlatformLegacyBundle\EzPlatformLegacyBundle to config/bundles.php. Symfony 3: register it in app/AppKernel.php. It must be loaded after the eZ Platform bundles because it decorates/extends their routing and view configuration.ezpublish_legacy in the standalone bridge; older eZ Publish 5 docs show it nested under ezpublish.legacy — verify with bin/console debug:config ezpublish_legacy):
ezpublish_legacy:
enabled: true
root_dir: "%kernel.project_dir%/ezpublish_legacy"
ezpublish_legacy/settings/siteaccess/<name>/, and a legacy_root_dir if you run more than one legacy tree.bin/console debug:router should show the legacy router in the chain, and hitting a known legacy URL (a legacy module/view, or content/view/full/<id>) should render through the legacy kernel instead of 404ing.First use case: the shortest path to value is not templating — it is URL ownership. Enable the bridge, let unmigrated URLs fall through to legacy, and confirm a legacy module (e.g. a custom extension/<ext>/modules/<module>/<view>.php) still serves through the Symfony front controller. That gives you a working "some URLs are Symfony, some are legacy" baseline you can shrink over time.
The core day-to-day mechanism: the bridge appends a legacy router last in the chain. Any URL you have not defined a Symfony route for is handed to the legacy kernel. Migration then becomes mechanical:
debug:router shows no legacy fallbacks in use.This is by far the safest pattern because it is per-URL reversible: delete the Symfony route and you are back on legacy. Resist the urge to do a big-bang cutover of a whole module.
The bridge provides a legacy templating layer plus a view provider so you can leave .tpl templates in design/<design>/templates/ and reference them from content_view configuration. This lets you port controllers and content types before you port presentation — you get the Symfony request/response lifecycle (and its cache and profiling) while the markup is still rendered by legacy. Typical flow: move the routing and controller layer first, then replace design: templates with Twig view by view.
Confirm the enabled provider/service IDs from the bundle's Resources/config/*.yml rather than guessing — the naming changed between eZ Publish 5's LegacyBundle and this EzPlatformLegacyBundle.
For logic you cannot yet reimplement (legacy workflows, custom extension data sources, legacy-only modules), the bridge exposes the legacy kernel as a service. Use the callback entry point rather than booting the kernel yourself:
// service: ezpublish_legacy.kernel (verify the ID with debug:container)
$result = $this->legacyKernel->runCallback(function () {
// legacy eZ Publish code runs here, with legacy autoloading and INI settings
return someLegacyFunction();
});
Rules for that callback body:
ezp* globals) into your Symfony code.runCallback() in a loop; every invocation pays kernel bootstrap cost.The bridge ships ezpublish:legacy:script, which executes legacy PHP inside a correctly bootstrapped environment:
php bin/console ezpublish:legacy:script bin/php/ezcache.php --clear-all
php bin/console ezpublish:legacy:script bin/php/runcronjobs.php
Use this for all legacy cron jobs and maintenance scripts instead of calling the legacy runcronjobs.php directly — otherwise the legacy process may pick a different DB/cluster configuration than the Symfony app and corrupt or miss data. Move legacy crontab entries to Symfony console entries as part of the migration.
legacy_root_dir is siteaccess-aware. That means you can point one siteaccess at the old tree (ezpublish_legacy) and another at a copy or a partially-customised tree, and migrate siteaccess by siteaccess rather than all at once. It is also the standard way to handle multi-site installs where legacy extensions differ per site. Verify the exact key (ezpublish.system.<sa>.legacy_root_dir vs ezpublish_legacy.system.<sa>.legacy_root_dir) with debug:config for your version — this is one of the settings that moved.
ezpublish_legacy.enabled: false disables the whole bridge. Keep it available — if a legacy boot starts causing 500s in production, this is your one-line rollback.test makes functional tests boot the legacy kernel (seconds per test) and introduces legacy filesystem/session side effects. Put enabled: false in config/packages/test/.ezpublish_legacy/extension/ and are autoloaded by the legacy autoloader. Do not add them to root composer.json and do not let PHPStan/PHP-CS-Fixer walk ezpublish_legacy/ — exclude the directory from all tooling and CI static analysis.ezpublish_legacy/ is code you intend to delete. Track migration progress as "count of legacy-routed URLs / legacy templates remaining", not as tickets.There are two caches, and you must clear both. This causes more wasted hours than anything else on this bundle. bin/console cache:clear does not clear legacy's ezpublish_legacy/var/cache/<siteaccess>/, and legacy caches INI settings aggressively. After any legacy settings/*.ini.append.php change:
php bin/console cache:clear
php bin/console ezpublish:legacy:script bin/php/ezcache.php --clear-all
Symptom of a stale legacy INI cache: "my config change had no effect" with no error anywhere. Always suspect this before debugging logic.
Two config systems, two logs, two error handlers. Legacy eZDebug::writeError() output goes to the legacy log under ezpublish_legacy/var/log/ and will not appear in the Symfony profiler or var/log/dev.log. When a legacy-routed page fails, check both. Legacy display_errors / EZP_DEBUG settings live in legacy's site.ini, not Symfony's .env, so "debug is on" in Symfony does not mean you get legacy stack traces.
The router order is load-bearing. Legacy is the last router in the chain. That is a feature (Symfony always wins) but also a trap: adding a broad Symfony route (e.g. a catch-all /{slug}) silently steals traffic from legacy pages, and the failure looks like a content bug, not a routing bug. When a legacy page suddenly renders as a Symfony 404, run debug:router and check for an overlapping pattern first.
Siteaccess names must match on both sides. ezpublish_legacy.system.<sa>.legacy_root_dir is useless if ezpublish_legacy/settings/siteaccess/<sa>/ does not exist. Symptoms are legacy falling back to the default siteaccess (wrong design, wrong INI, wrong language) rather than an error. Mismatched siteaccess is the second-most-common cause of "legacy behaves differently through the bridge than it did standalone."
Webserver rewrite rules must cover legacy asset paths. Legacy serves /design/..., /extension/..., /var/... (storage) directly from files under the legacy root. Behind the Symfony front controller these paths often need explicit nginx/Apache rules or they 404. Check this before blaming the bundle when images and CSS are broken but HTML renders. Budget for updating vhost config as part of the cutover.
File handlers / cluster configuration is duplicated. Legacy reads file.ini (var/storage, DFS/cluster settings) while eZ Platform uses its own dfs/IO configuration. If a legacy page shows missing images but the Symfony side is fine, the two configurations disagree. Legacy content files and platform binaries live in the same storage in eZ Platform 2.x, so a mismatch can produce silent 404s rather than errors.
Performance: legacy boot is expensive and all-or-nothing. Every request that touches the legacy kernel — whether through routing or a runCallback() — pays a full eZ Publish bootstrap plus legacy cache reads. There is no partial boot. Consequences:
runCallback() in Symfony cache when the underlying legacy data is stable.Sessions and authenticated users can desync. If login is handled by Symfony and a legacy page needs the user, the legacy kernel must be handed the right identity; otherwise you get "logged in on some pages, anonymous on others." If you see this, check the bundle's user/session bridging service rather than debugging legacy's auth code — and prefer keeping all authentication in Symfony with legacy pages reading the injected user.
Upgrade trap. Because the package is archived, composer update will happily keep it pinned while the rest of your dependencies move on, and the eventual eZ Platform/Ibexa major upgrade will break it with no upstream fix. Plan the bridge's removal — not just the legacy code's removal — as an explicit milestone in the migration, and verify the supported eZ Platform version in the bundle's composer.json constraints rather than the README before planning any platform upgrade.
Extension points, in the order you should reach for them: (1) a Symfony route shadowing a legacy URL; (2) a legacy template used as a content view while the controller is Symfony; (3) runCallback() inside a thin adapter service; (4) a custom legacy module. Anything below level 2 increases your legacy surface area and should have a delete-by date attached.
How can I help you explore Laravel packages today?