Article

Migrating Zend Framework to Laminas (Complete Guide)

Deep, verified migration from ZF2/ZF3 (and Apigility/Expressive) to Laminas using laminas-migration: Composer ≥1.7, backup, --exclude, install, and test.

Laminas is the official open-source continuation of Zend Framework after ZF was archived and abandoned. For applications on the ZF version 2 or 3 series—and for Apigility or Expressive stable apps—the Laminas Project documents a mechanical migration via laminas/laminas-migration. This flagship walks that path end-to-end from the official documentation at docs.laminas.dev/migration/. ZF1 is out of scope for this tool; see ZF1 migration planning and ZF1 + PHP 8 reality check.

0. Confirm you are in scope

Official migrate targets (from Laminas docs):

  • Individual projects or applications that depend on Zend Framework, or have dependencies on components that depend on Zend Framework, where the release is in the version 2 or version 3 series
  • Individual projects or applications built on Apigility, all stable versions
  • Individual projects or applications built on Expressive or using Expressive components, all stable versions
  • Libraries that depend on Zend Framework components, versions 2 and later, or created after Zend Framework 2.0.0 was originally released

If you still bootstrap with Zend_Application, underscore class names like Zend_Controller_Action, and a vendored library/Zend tree, stop—this guide is not your primary path. Identify your generation with versions overview.

1. Preparation: Composer ≥ 1.7 and version control

Due to features of Composer the dependency plugin uses, Laminas requires Composer 1.7.0 and up. Check with composer --version. If you are on an older version, run composer self-update. The docs assume you use Composer to install Zend Framework—it is the only supported mechanism currently.

Put the application under version control before migrating. The migration tool changes source code, updates templates, modifies your composer.json, and removes your composer.lock and vendor/ subdirectory, among other things. If you want to roll back after a problem, make certain your code is under version control. Laminas recommends Git.

# Official-style bootstrap if you are not yet in VCS
cd path/to/your/application
git init .
echo "vendor/" >> .gitignore
git add .
git commit -m "Initial import before laminas-migration"

composer --version
# composer self-update   # if below 1.7.0

If you already use Git, start from a clean working tree and a named branch so git diff / git reset --hard remain viable rollback tools.

2. Install laminas-migration globally — never as a local dependency

Official warning: Do NOT install locally. While laminas/laminas-migration is a Composer package, it cannot be used as a local requirement in your application. Part of its operation removes the vendor/ subdirectory, which means it removes itself during operation. As classes are loaded dynamically as needed, this can cause the code to error during later operations when classes are unavailable, leading to a failed migration.

Two supported install approaches:

  1. Global Composer requirement (recommended)
  2. Cloning the laminas/laminas-migration repository and installing its dependencies separately
# Recommended
composer global require laminas/laminas-migration

# Locate global home and ensure vendor/bin is on PATH
composer global config home
# Linux/macOS example — adjust to your global home:
# export PATH="$(composer global config home)/vendor/bin:$PATH"
# Alternate: clone
git clone https://github.com/laminas/laminas-migration.git
cd laminas-migration
composer install
# then PATH, symlink, or shell alias to bin/laminas-migration

3. Run migrate with exclusions and filters

Enter the project and run:

cd path/to/your/application
laminas-migration migrate

You may want to use the --exclude / -e option one or more times for directories to exclude from the rewrite, or the --filter / -f option one or more times to provide regular expressions of which files to include. This is especially useful if you have folders with large amounts of files (for example images or static assets). Excluding such directories can drastically improve execution time and avoid pointless rewrites of binaries.

# Example from official docs
laminas-migration migrate -e data -e public/images

# Help for all options
laminas-migration help migrate

Typical exclude candidates in real apps: data/cache, upload directories, generated proxies, front-end build output, and vendor-like folders that are not Composer’s vendor/ (the tool removes Composer’s vendor/ itself as part of migration).

4. Caution: --keep-locked-versions

If you have strict upgrade paths and do not want to upgrade packages to their latest versions, you might use --keep-locked-versions, which ensures that any package from composer.lock is added with the currently installed version to your composer.json.

Laminas documentation encourages migrating the non-conservative way to avoid unexpected issues with older versions of the migrated Laminas packages. If you experience issues after migration with this flag, they note they cannot offer support as they highly encourage upgrading to the latest packages.

laminas-migration migrate -e data --keep-locked-versions

Post-migration cleanup when the flag was used: diff composer.json, restore old package constraints where appropriate, and remove packages that were not required on the project level before migration. After cleaning composer.json, update the lock with:

composer update --lock

5. Verify the diff before reinstalling

git diff
git status

Things the official docs suggest looking for:

  • Renaming of files or classes in your own code that reference ZF components. Example: My\Models\ZendMailTransport might become My\Models\LaminasMailTransport. Such renames are typically okay, and references are rewritten as well—but if external code depends on those class names, verify.
  • Changes to configuration keys. Most match changes in Laminas libraries themselves, but check keys specific to your own code.
// LEGACY
use Zend\Mvc\Controller\AbstractActionController;
use Zend\ServiceManager\ServiceManager;

// MODERN (post migrate)
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\ServiceManager\ServiceManager;

6. Install dependencies

Once migration is done and you have performed verification steps you need, install dependencies:

composer install

Remember: the tool removed vendor/ and composer.lock as part of its operation. A fresh install is mandatory. Validate platform PHP and extensions with Composer platform checker if the new constraints raised your runtime floor.

7. Test thoroughly

Official guidance is blunt: run your unit tests, do end-to-end tests—whatever you have—but exercise the application in some way. Practical minimum for a ZF2/ZF3 → Laminas cutover:

  • PHPUnit / integration suite green
  • Smoke routes: home, login/logout, a representative CRUD module, file download if present
  • CLI / cron entry points and queue consumers
  • Clear config and proxy caches if your app uses them
  • Compare Composer platform requirements against production PHP using environment compare

If anything does not work, determine the specifics and report either in the repository of the specific component where you observe problems, or via Laminas community channels listed in the official docs (Slack/forums).

8. Rollback strategy

Because the tool rewrites sources and deletes lock/vendor, rollback is a VCS operation—not a panel toggle:

  • Before migrate: named branch + tag on the last known-good ZF commit
  • After a bad migrate: git reset --hard / restore branch, then composer install from the restored lockfile
  • Never run the first migrate only on production hosts
  • Keep database migrations separate; namespace rewrites should not require irreversible schema changes in the same window

9. Third-party packages and nested ZF dependencies

Many applications pull packages that still declare zendframework/* dependencies. The migration tool is designed to rewrite projects and libraries in scope, but you should still:

  • Inventory direct and transitive ZF packages before and after (composer show)
  • Check whether third-party packages already published Laminas-compatible releases—prefer those over indefinite forks
  • Treat abandoned packages as a separate workstream (abandoned packages)
  • Avoid mixing long-term ZF and Laminas duplicates in one app after cutover

Composer modernization context: Composer center.

10. Runtime and tooling alignment after namespaces move

Summary command sequence

Official summary shape:

composer global require laminas/laminas-migration
cd some/project
laminas-migration migrate   # often: -e data -e data/cache
composer install
# test thoroughly

Primary source: https://docs.laminas.dev/migration/. Companion pages: from ZF to Laminas, migration tooling, ZF2 center, ZF3 center.

What the tool actually rewrites (expectations)

Mentally model laminas-migration as a project rewriter plus Composer metadata updater—not as a semantic redesign of your architecture. Namespaces, use statements, many configuration keys, and package names move from Zend Framework naming to Laminas naming. Your module structure, routing ideas, and custom services remain your responsibility to validate. That is why the official docs insist on tests after composer install.

Because composer.lock and vendor/ are removed, the next install resolves packages according to the rewritten composer.json (unless you used --keep-locked-versions). Teams sometimes underestimate how much changes when transitive dependencies float. Staging must install from the post-migration metadata, not from a copied pre-migration vendor/ tree.

PATH and operator pitfalls on shared build agents

Global Composer installs vary by user. CI users, deploy users, and interactive developers may not share the same global home. Before a migration day:

  • Run composer global config home as the same user that will execute migrate
  • Confirm which laminas-migration resolves
  • Document the PATH export in the runbook next to the migrate command
  • Prefer cloning the tool into a known path on locked-down build images if global Composer is restricted
# Verify the binary the runbook will call
command -v laminas-migration
laminas-migration help migrate | head

Apigility and Expressive notes

Official scope explicitly includes Apigility stable applications and Expressive applications/components. Still treat them as products with their own configuration shapes: API-oriented modules, authentication adapters, and middleware pipelines need smoke tests beyond “MVC page loads.” After migration, confirm package names in composer.json show laminas/ (and related project namespaces) rather than leftover zendframework/ pins you intended to replace.

Third-party packages that still speak Zend

Not every package in the ecosystem moved on the same day. After migrate + install:

  • Run composer show | grep -E 'zendframework|laminas' and reconcile surprises
  • For packages that remain on ZF names, check whether a Laminas-compatible major exists upstream
  • Avoid long-term dual stacks where both ZF and Laminas copies of the same component load
  • Track abandoned dependencies separately via abandoned packages

Integration with PHP upgrades

Namespace migration does not fix PHP 8 engine breaks in your application code. Sequence deliberately:

  1. Get tests green on the current PHP with ZF packages
  2. Migrate to Laminas on that same PHP when possible
  3. Then raise PHP using PHP 7→8 / checklist guidance

If you must combine changes, increase test coverage first; combined failures are expensive to bisect. Validate platform packages with platform checker after each step.

FAQ-style failure modes

  • Tool disappeared mid-run — you installed laminas-migration as a local project dependency; reinstall globally/clone and restore from Git.
  • Huge runtime / weird binary diffs — add -e excludes for assets and generated data.
  • App class renamed unexpectedly — review diff for Zend substrings in your own class names; rename carefully if external systems depend on them.
  • Install pulls unexpected majors — consider a carefully reviewed --keep-locked-versions pass, then clean constraints as docs describe.
  • ZF1 project — stop; use ZF1 + PHP 8 containment planning instead.

Documentation trail to keep with the PR

Attach to the migration pull request: Composer version used, exact migrate command (including every -e), whether --keep-locked-versions was set, PHP version of the install job, and a link to the official Laminas migration page. Future maintainers should not have to reverse-engineer whether the lockfile was hand-edited after a conservative migrate. Companion tooling notes live in migration tooling.

Related reading