PHP 8.6 arrives on November 19. DressCode already rewrites code for it. 10 days Upgrading to a newer PHP →

The Upgrade Plan Step by Step

How much effort will it actually take to upgrade to a newer Nette, Symfony, Laravel, or PHP? DressCode measures the scope before touching a single line of code: calculating what it can rewrite automatically, what must wait until dependencies are raised in composer.json, and what requires manual attention. It then builds a step-by-step upgrade plan and executes it, including dependency updates via Composer. And when you tell it how to run your tests, it reverts every step after which one of them fails.

The whole process is driven by three commands: dresscode upgrade estimate measures the work, upgrade plan builds the execution plan, and upgrade run carries it out. What an upgrade does to your code, how DressCode knows what changed, and a shorter way for when you have already raised the versions yourself are described on the Upgrading Code page. Every option is detailed in the command line reference.

Before You Start

DressCode requires PHP 8.4, so in an application on an older PHP or on Nette 3, install it globally together with PHPStan and the upgrade package, as described in Where DressCode Runs.

The plan rewrites code in the style the configuration of the project keeps, so it needs a dresscode.neon file. If the project does not have one yet, dresscode init measures it from your code; without a configuration, upgrade run refuses to start. And start with a clean Git working tree, so that the upgrade changes do not blend with unrelated work.

How Much Work an Upgrade Takes

Take a Nette 3 application moving to nette/utils 4.0. To estimate the effort, all you need to supply is the target package and version, or the keyword php and a PHP version:

dresscode upgrade estimate nette/utils 4.0
DRESS|CODE 1.3.0
Config     /var/www/shop/dresscode.neon
Upgrade    nette/utils 4.0 over 3 files

Must change, 2 topics with 12 findings
  nette/utils  10 findings in 3 files    needs ^4.0, automatic
  nette/utils  2 findings in 1 file      works on ^3.2, automatic

Today      2 findings of `deprecations` are rewritten on the versions allowed now by `dresscode upgrade today`
Composer   nette/utils 4.0: nothing in the lock refuses it
Moves      nette/utils v3.2.10 => v4.0.0
Data       nette/application up to 3.3, nette/component-model up to 3.2, nette/http up to 3.4,
           nette/routing up to 3.1.4, nette/utils up to 4.1.5, php up to 8.6

Each line in the output represents a single topic: either a library for which DressCode has upgrade data (here nette/utils), or a specific decision for a change that no upgrade data describes. Next to the topic name, you see the number of findings (locations requiring modification), the number of affected files, and two descriptors outlining the nature of the edits.

The first descriptor specifies when an edit can safely be made. The label works on ^3.2 means the updated code is fully backward compatible with the constraints currently permitted by your composer.json (here ^3.2), allowing DressCode to rewrite it immediately. Conversely, a location marked needs ^4.0 must wait until you raise your dependency constraints, because code written for the new API would break on the older version. This is why a single library can appear on both lines: Nette deprecated Arrays::searchKey() back in the 3.x series and its replacement getKeyOffset() was introduced in 3.2, whereas Strings::startsWith() was removed entirely in 4.0.

The second descriptor identifies who handles the rewrite. DressCode resolves automatic items completely on its own. Items flagged as manual represent developer tasks: places where a human must evaluate the context and choose a replacement (the accompanying report explains exactly what to write). Locations marked unknown types are waiting on static analysis from PHPStan, while uncertain names indicates that the place waits until it is certain which function a call actually targets.

The Today line highlights what running upgrade today can fix right now. The Composer line warns of potential conflicts in composer.lock: if another package blocks the target version, it is listed here along with its conflicting constraint. The Moves line shows which packages a constraint bump would advance to a new major version, based on a Composer dry run. The final Data line lists all libraries covered by available upgrade packages and the version the data reaches for each.

The estimate command is entirely passive and modifies no files. Omitting the target proposes the latest versions known to the data sets; running dresscode upgrade estimate php does the same strictly for PHP.

In an interactive terminal, the estimate prompts whether to enable static type analysis, since types determine what object a method is invoked upon. Pass --types to enable analysis without prompting, or --no-types to estimate without it (in which case type-dependent locations are categorized as unknown types).

The estimate asks Packagist which upgrade packages exist (see Missing Data), and Composer whether the lock blocks the target and what would move; the last query needs network access, and --offline skips it. A transition where upgrade data is unavailable is marked (no data), and a library the data covers only up to an earlier minor release shows (data up to 4.1). Files the check cannot read, for example ones that do not parse, are listed under Unverified and excluded from the count. When the estimate leaves a file unprocessed, lacks upgrade data or gets no answer from Composer, it exits with code 2, so that an incomplete estimate is never mistaken for a complete one.

The Upgrade Plan

The upgrade plan command prints the estimate followed by a structured, phased execution plan; passing --save saves this plan directly to disk. The output below summarizes the estimate with ...:

dresscode upgrade plan nette/utils 4.0 --save
DRESS|CODE 1.3.0
Config     /var/www/shop/dresscode.neon
Upgrade    nette/utils 4.0 over 3 files
...
Mode       application, `composer.json` gives the type `project`
Plan       nette/utils 4.0
  1  fix       works on the versions allowed today  nette/utils
  2  composer  raise nette/utils to ^4.0
  3  fix       needs the targets                    nette/utils
  4  composer  update to the newest versions
Tests      not run, no `--test-command` given
Saved      dresscode-upgrade.neon
Next       `dresscode upgrade run` carries out the plan saved, step by step

The plan progresses from backward-compatible rewrites that work today (the bridge), through raising constraints in composer.json, to rewrites that target the new API (the jump). Following the jump comes dead code removal (such as PHP_VERSION_ID < 80100 checks that are always false once running on PHP 8.4) and an optional step running the modernizations set to adopt newer PHP syntax. In our example above, those later steps are omitted because there is nothing for them to do. An application plan concludes by updating packages to the newest versions permitted by the constraints; the Mode line says that the project is an application, and why. Libraries are planned differently (see below).

The generated dresscode-upgrade.neon file in your project root is standard NEON and belongs in version control: your team can review it, skip steps by setting skip: true, narrow topics, or attach your test command. DressCode reads the plan strictly, so a typo in it is reported instead of silently altering the upgrade. Each step is a recipe, not a frozen result: it defines which topics to migrate and how to adjust constraints, calculating the exact number of file modifications dynamically when it reaches that step:

version: 1

targets:
	nette/utils: '4.0'

steps:
	-
		title: works on the versions allowed today
		kind: fix
		topics:
			- nette/utils

	-
		title: raise nette/utils to ^4.0
		kind: composer
		require:
			nette/utils: ^4.0

	-
		title: needs the targets
		kind: fix
		topics:
			- nette/utils

	-
		title: update to the newest versions
		kind: update

The kind: update step brings dependencies to the latest releases allowed by your constraints, completing when composer update --dry-run reports no further changes.

Any step that cannot be done yet is preceded by a manual intervention point, labeled manual. For example, if a locked dependency blocks the target version, the plan begins with the command needed to diagnose the conflict, such as composer why-not nette/utils 4.0. Running the plan halts at that step with exit code 4, resuming only after Composer confirms that the conflict has been resolved. In a project without a composer.lock, Composer cannot be queried, so the runner pauses similarly; if you know a step is clear, bypass it by setting skip: true. Anything the initial estimate could not verify is saved under the estimate key (files it could not process under unprocessed, missing upgrade packages under missingData); when executing such a plan, the runner warns about these unknowns up front and checks the whole project at the end.

Carrying Out the Plan

Running upgrade run with a target saves and executes the plan immediately; running it without arguments executes an existing saved plan. Each topic is upgraded independently and written in the style your code follows. Here is the terminal output from running dresscode upgrade run for the plan above:

DRESS|CODE 1.3.0
Config     /var/www/shop/dresscode.neon
Step 1     nette/utils 1 file
Waiting    step 2, raise nette/utils to ^4.0: run `composer require "nette/utils:^4.0" -W --prefer-lowest --minimal-changes`, then `dresscode upgrade run`

The bridge step completed, and the runner paused before invoking Composer, because outside an interactive terminal it never modifies package files without consent. In an interactive terminal, it presents the planned package changes and asks for confirmation before editing composer.json and updating dependencies; non-interactively, it proceeds only if passed --yes. Otherwise, it leaves Composer untouched, prints the exact composer require command, and exits with code 4, indicating that it is waiting on an external action. You can run Composer manually in your terminal and then call upgrade run again. With --yes, the runner continues seamlessly:

DRESS|CODE 1.3.0
Config     /var/www/shop/dresscode.neon
Warning    The working tree has changes of its own, which the changes of the upgrade mix with.
Composer   raise nette/utils to ^4.0, which changes:
  - Upgrading nette/utils (v3.2.10 => v4.0.0)
Composer   update to the newest versions, which changes:
  - Upgrading nette/application (v3.1.15 => v3.3.0)
  - Upgrading nette/component-model (v3.0.3 => v4.0.2)
  - Upgrading nette/http (v3.2.4 => v3.4.2)
  - Upgrading nette/routing (v3.0.5 => v3.1.5)
  - Upgrading nette/utils (v4.0.0 => v4.1.6)
Step 1     nette/utils left nothing to fix
Step 3     nette/utils 3 files
UPGRADED  4 steps done, and the final check finds nothing more to upgrade, no tests run

The warning at the top indicates that edits from step 1 were not yet committed, meaning they will blend with the changes from the jump step. Committing after each stop clears the warning.

Composer installs the lowest versions permitted by the new constraints (--prefer-lowest --minimal-changes), because code at this stage is written for them and DressCode inspects their @deprecated annotations (see Lower Bounds Decide). Other packages are updated only as much as necessary, and DressCode explicitly flags any package that would be downgraded relative to the lockfile before you confirm. Steps that raise the PHP version also update config.platform.php if defined.

The newest package versions arrive in the final step (update to the newest versions), which is tested like any other; skipping it with skip: true leaves the lowest compatible versions installed, which the final report will note. The UPGRADED line reports completed and skipped steps, confirming that the final check of the entire codebase found no remaining deprecated API usage, see below. What the code looks like afterwards is shown in the example on the Upgrading Code page.

Notice that step 1 performed no modifications during the second run. DressCode determines current progress directly from the code and lockfile rather than relying on state files: a topic with nothing left to rewrite is complete, and a Composer step is satisfied when composer.json contains the constraint and the installed version matches. As a result, upgrade run resumes reliably even if your team squashes or reorders commits in Git, or if you run Composer commands manually.

Following a Composer update, execution continues in a fresh DressCode process so that subsequent rewrites, test runs, and final checks immediately see the updated vendor/ directory, including new plugins and upgrade data. The only exception is when DressCode runs inside another tool that captures its output: in that context, it avoids spawning processes, pauses after Composer finishes with exit code 4, and prompts you to invoke dresscode upgrade run again.

Tests and Commits

DressCode runs your test suite only when explicitly instructed how to invoke it. The --test-command option accepts your project's test command, where {junit} serves as a placeholder for a JUnit XML report file (a format supported out of the box by both Nette Tester and PHPUnit):

dresscode upgrade run nette/utils 4.0 --test-command "vendor/bin/tester tests -o junit:{junit}"

Before executing the first step, DressCode runs the tests and records which ones passed. If a test that previously passed fails, is skipped, or disappears after a step, DressCode reverts that step's file edits byte for byte (leaving any files you modified manually intact) and exits with code 1. Any dependencies updated by Composer remain raised; the output lists them under Raised, and you can revert them via dresscode upgrade abort (see below).

You can supply a faster subset of tests for low-risk intermediate steps via --quick-test-command; it is also compared with its own results from before the first step, while the full suite runs after the major jump and after the last step. The Tests line in the plan indicates which command runs after each step.

Test runs that fail to generate a valid JUnit report or exceed timeout thresholds are always treated as failures, never passes. If tests cannot run even before step 1 begins, execution halts before changing any files, with exit code 2 and the reason. The Tests line of the run says what the tests were compared with: when they ran before the first step and how many of them failed even then, because having no new failures is not the same as having all tests pass. A test command lacking {junit} is rejected during plan construction, and also in a plan file edited by hand.

Commits are left to you by default. With --commit, DressCode creates an isolated Git commit for each topic as soon as its step passes the test suite. It builds commits directly from topic outputs using its own Git index without altering your working tree state, and all repository Git hooks execute normally. Composer dependency updates receive dedicated commits as well (unless composer.json and the lockfile are ignored by Git), so the next run does not mistake those files for unfinished work. Because commits capture entire files, --commit is refused up front if tracked files have uncommitted changes or if Git cannot find a tracking repository.

If a file is modified externally after a step writes it (whether by tests or by you), committing it automatically would commit unverified code. In that scenario, the step remains uncommitted in your working tree and the runner prompts you to commit it manually. If Git rejects a commit (for example, due to a pre-commit hook), the runner halts with exit code 2 and reports what remains uncommitted and which topics of the step it had already committed. A step that left files it could not process stays uncommitted until a later run processes them and commits the whole step. If you edit the plan file so a step does something different, the runner refuses to proceed: commit the step's changes yourself, and use upgrade run --discard to drop the runner's cached memory of that step.

Manual Tasks Along the Way

Anything the automated fixer cannot safely resolve is listed under the steps as a manual task (the Manual line), complete with file coordinates and instructions on what to write instead. As long as any manual task remains, execution halts after the major jump and at the conclusion of the plan with exit code 1; a plan is only complete when zero manual tasks remain. Resolve each location manually, or confirm the existing code with an explanatory comment, and run upgrade run again:

// dresscode:ignore upgrading.php.deprecatedApi -- the report needs the locale of the server
$month = strftime('%B');

An inline suppression with a reason is immediately visible in code review diffs, making the justification clear to colleagues. Locations you intend to keep indefinitely can also be added to your baseline.

Running dresscode upgrade tasks inspects the codebase against the saved plan and prints all remaining manual tasks (including those detected by the whole-codebase scan at the end of the plan) without modifying any files. Under Exceptions, it flags dresscode:ignore comments that lack an explanatory reason, which merely postpone tasks and are not considered accepted exceptions, and for risky fixes it details what the fix is waiting on and how to approve it. The command exits with code 1 while tasks or comments without a reason remain, making it ideal for CI checks. Until the plan's raise of the constraints is made, it lists only what can be done with the versions the project requires today, and its last line names the step it waits for (OK no manual task before step 2, raise php to ^8.4; under due in the JSON document).

At the end of the plan, DressCode scans the entire project using the deprecations set, going beyond the topics flagged in the initial estimate. This catch-all pass is where newly updated libraries installed during Composer steps can reveal newly deprecated APIs. If findings emerge, the run exits with code 1 and lists files for dresscode upgrade today or remaining manual tasks. Resolve them and rerun upgrade run. A plan completed without some of the upgrade data says so on its last line.

Any file that cannot be processed (for example, due to a syntax error) is listed under Unverified, and the runner halts after that step with exit code 2, since an incomplete run cannot be considered successful.

Interrupted Runs and Going Back

If a run is interrupted mid-step, some files on disk may already have been modified. The next invocation lists them and halts without performing work: restore them via dresscode upgrade abort, or proceed from current disk state with upgrade run --discard. Running upgrade plan --save pauses similarly, preventing a new plan from overwriting state from an unfinished step.

The dresscode upgrade abort command restores everything saved for rollback: it reverts files modified during the interrupted step, restores composer.json and composer.lock to their exact state prior to the first Composer update, and runs composer install to synchronize your vendor/ directory with the restored lockfile. Any file modified manually in the interim is left untouched and reported under Left. Run upgrade abort --dry-run to preview what would be restored without writing changes.

These backups are held until a subsequent post-update step finishes successfully, and the Composer files are restored even if located above the configuration directory. Whenever execution halts with modified Composer files (such as when tests fail after an update or Composer crashes), they are listed on the Raised line; Composer failures are reported alongside the final line of its output and a path to a detailed log file in the temporary directory. When abort leaves a file unchanged or composer install fails, it exits with code 1.

To check where a saved plan currently stands without running fixes, use dresscode upgrade status. For Composer steps, it inspects constraints to determine if they are pending or complete; for manual steps, it checks the lockfile to see if conflicts have cleared. It does not recalculate remaining code fixes, leaving that to upgrade run. Below the plan, it summarizes backup state: interrupted steps, updates abort can revert, uncommitted steps, and when the tests ran before the first step. It always exits with code 0:

DRESS|CODE 1.3.0
Config     /var/www/shop/dresscode.neon
Plan       nette/utils 4.0
  1  works on the versions allowed today  what is left `upgrade run` fixes and `upgrade tasks` lists
  2  raise nette/utils to ^4.0            done
  3  needs the targets                    what is left `upgrade run` fixes and `upgrade tasks` lists
  4  update to the newest versions        `upgrade run` tells
Raised     step 2 changed composer.json, composer.lock, which `dresscode upgrade abort` puts back
Next       `dresscode upgrade run` carries on from where the code stands

Rollback data is stored directly in your Git repository under .git/dresscode-upgrade/, ensuring it survives cache purges. Projects without Git fall back to the cache directory, which triggers a warning. Two upgrade runs cannot execute concurrently on the same project: a second process is refused until the first completes.

A Library Widens, It Does Not Raise

Libraries (projects configured with "type": "library" lacking their own composer.lock) do not raise minimum version constraints: library code must remain compatible with the older versions it currently supports. A library plan therefore consists only of the bridge step and a widened constraint that includes the target (for example, expanding ^3.2 to ^3.2 || ^4.0), written using --no-update. Rewrites that require the newer target API exclusively are neither applied nor reported at this stage, because the library code must keep working with the older version (see Lower Bounds Decide). For projects that declare type: library but nevertheless maintain a lockfile, DressCode prompts for clarification in an interactive terminal and follows type non-interactively; the --library and --no-library options settle this behavior explicitly.

How to exclude the tests of a library that call the old API on purpose is described in Upgrading a Library You Maintain.

Where to Go Next

Show on GitHub · Report a problem with this page on GitHub · Nov 7, 2026