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
- Upgrading Code, what an upgrade does to your code, why lower bounds decide, and
how to upgrade with the single command
upgrade today. - Nette libraries, Symfony, Laravel, and Dibi and Texy, what their packages cover.
- Upgrading to a Newer PHP, when raising your PHP version alongside your libraries.
- Types from PHPStan, which enable safe refactoring of constants, methods, and member calls.