Upgrading Code
You moved to a newer Nette, Symfony, Laravel or PHP, and the old API is still in your code. Raise the versions in
composer.json, run dresscode upgrade, and the code is rewritten to the API of the versions the project
now requires: with no configuration, in the style the project already has, and with a diff that holds nothing but the upgrade.
What cannot be rewritten is reported, along with a sentence saying what to write instead.
An Upgrade in One Command
An upgrade starts in composer.json: raise the constraints to the versions you are moving to, because the code is
written for the lowest version a constraint allows, see below. Then DressCode needs two
things: data about what the new versions of the libraries changed, and PHPStan, which it asks about types. Add both to the project
as development dependencies; the upgrade package brings DressCode with it:
composer require --dev dresscode/upgrade-nette phpstan/phpstan
vendor/bin/dresscode upgrade
Instead of upgrade-nette, take the package of your ecosystem: upgrade-symfony,
upgrade-laravel, or upgrade-deegee for Dibi and Texy. The table of packages shows what each one carries.
With DressCode installed globally, install PHPStan next to it,
composer global require dresscode/dresscode phpstan/phpstan. Then you do not have to install the upgrade package:
upgrade detects which libraries the project uses and offers the missing data itself, see below.
What dresscode upgrade does:
- It needs no configuration. It takes the whole project directory, and the versions of the libraries and of PHP from the
constraints in
composer.json, see below. - It rewrites only the old API. It runs the set
deprecations: the data of the installed upgrade packages with their rules, plus the data about what PHP has deprecated. With the option--modernize, it also rewrites the code to newer PHP constructs (the setmodernizations). - It does not touch the style. Without a configuration it has no style; with a configuration, it takes from it only what the code already follows, see below.
- It asks PHPStan about types, because without them it cannot tell what object a method is called on. So without PHPStan installed, it stops with an error.
- It tells you what is left, and if it rewrote something, it advises you to run it again.
This is what an upgrade of a Nette application looks like:
DRESS|CODE 1.0.0 Config /var/www/shop/dresscode.neon Target PHP 8.2 from `composer.json` Upgrading nette/application, nette/component-model, nette/forms, nette/http, nette/utils Fixing 214 files in /var/www/shop app/Presentation/Product/ProductPresenter.php rewritten app/Presentation/Sign/SignPresenter.php rewritten app/Model/Referrals.php error 27:26 Method `Nette\Http\Request::getReferer()` is forbidden: use `getOrigin()`, which reads the Origin header and gives only the scheme, the host and the port instead of the whole URL of the referring page. upgrading.api.forbiddenMembers FIXED 18 violations found, 1 remaining in 3 of 214 files, 3.2 s Run `dresscode upgrade` again: the code it rewrote may hold more to upgrade.
The line Upgrading names the libraries whose data applies. DressCode rewrote the files marked
rewritten; under each of the other files, the output lists what is left to do. The last line advises a second run,
because the rewritten code may hold another thing to upgrade that the first run could not see yet. A run that rewrites nothing
more does not print this advice.
A Diff You Can Trust
upgrade has no preview; git shows the changes. So start with a clean working tree and read the diff after the run.
You will find only the places where the old API was, rewritten into the shape the surrounding code has, imports included.
DressCode does not search and replace text. It rewrites a method call only where the types say the method belongs to that class:
getName() is rewritten on FileUpload and nowhere else. How Upgrading Libraries Works explains the mechanism and how DressCode
knows what has changed. Want to start carefully? Run upgrade on one file first,
dresscode upgrade app/Model/Orders.php, and look at what it did.
Read the diff anyway. The data describes how code is written, not what it means: when a new version returns -1
instead of null, DressCode rewrites the name of the method but leaves the condition === null as it is.
So after the upgrade, run your tests and PHPStan.
Your Style Stays
A project without dresscode.neon has no style, and upgrade changes only the old API. If the project
has a configuration, everything in it that concerns the upgrade applies: paths and excludePaths, the
target PHP version, fixRisky, the baseline, the overrides in overrides, and your own replacement maps. From the style, only
the parts the code already follows apply: upgrade first checks the code against the style of the project and leaves
out every decision the code violates somewhere. So new code takes the form the project really uses, and a file the upgrade touches
is not reformatted.
The unit is the decision, and the choice holds for the whole project: a single long line anywhere leaves out
file.longLines everywhere. If you want the style as well, run dresscode fix afterwards, ideally as a
separate commit.
Missing Data
upgrade learns from Packagist which upgrade packages exist, for now only from the vendor dresscode.
It asks at most once a day and remembers the answer in the cache directory, so it also works offline; with no network and no
remembered answer, it simply offers nothing. It offers a package when the project itself requires one of its libraries in
require or require-dev, not when the library came only as a dependency of another one:
The libraries of the project have upgrading data in `dresscode/upgrade-nette`, which DressCode does not have. Install it beside DressCode in `C:/Users/me/AppData/Roaming/Composer`?
The package goes into the DressCode installation, so the composer.json of the project stays as it is. If DressCode
is a dependency of the project (in its composer.lock), the package is installed into the project as
composer require --dev, and the question says that the composer.json of the project will change. Without
a terminal, in CI or in an agent hook, upgrade installs nothing: it ends with exit code 3 and prints the exact
Composer command. The option --yes allows the installation even there.
What Is Left to You
An upgrade is a rewrite plus a list of what is left, and that list is half of its value. DressCode does not guess: where there is no replacement, or a human has to choose it, it reports the place and says what to write instead:
Constant `Nette\Application\IRouter::SECURED` is forbidden: write the protocol, `https://`, into the mask of the route.
Method `Nette\Http\UrlScript::setPath()` is forbidden: the class is immutable; take a changed copy with `withPath()`.
Each such message is an instruction, so you can go through the list yourself, or hand it to an AI agent as it is; with the
option -f agent the agent gets it in sentences written for a model, see AI agents. DressCode also reports places where the data
offers a fix that cannot be applied there, for example because it would throw away the expression the method is called on, or
because a comment stands between the arguments. The message gives the reason, and the code stays as it was.
Risky fixes wait for your permission. A few rewrites could change what the code does, for example reading a property on
the left of ??, where PHP first asks whether it is set. The summary counts such fixes, and
dresscode upgrade --ask-risky goes through them with you one by one, showing the diff of each.
--fix-risky applies all of them, --fix-risky=upgrade-nette only those from the data of one package, and
the decisions you trust for good go into the key fixRisky.
What the data does not see. The data does not capture changes in behavior, configuration in NEON or the syntax of templates; the upgrade guides of the individual libraries describe those. And when a library deletes something without a record in the data, only PHPStan will find it.
Lower Bounds Decide
The version the code is rewritten for is not decided by the version installed in vendor, nor by the PHP DressCode happens to
run on, but by the constraints in composer.json. The code must work with every version a constraint allows, so it is
written for the lowest one: code written for a newer version would break wherever the constraint lets in an older one. With
"nette/forms": "^3.2", the code is therefore rewritten to the API of version 3.2, even when 3.3 is installed in
vendor. DressCode reads the PHP version the same way, from require.php. Composer does not raise the lower bound of
PHP by itself, even when the project runs on a newer one, so the key config.platform.php raises it: with
^8.1 and the platform 8.3, the code is written for PHP 8.3.
So upgrading does not mean installing a newer version, but raising the lower bound. As long as the constraint allows the old version, DressCode does not rewrite the code to the new one, whatever is in vendor.
Changes from higher versions that the constraint also allows are not rewritten but reported, because the code will run on them
one day. With "^3.2 || ^4.0", the code is rewritten to the API of version 3.2, and what 4.0 changed is reported
together with the version:
Class `Acme\Old` is deprecated: replaced by `Acme\New`, since acme/lib 4.0.
The word deprecated says the installed code still has the thing; removed says it no longer does.
A library the project requires without a lower bound (*), or does not require at all and got only as a dependency of
another one, can be in any version. So its data rewrites nothing; everything is only reported. The header of the
upgrade output lists such libraries on the line Reported, and on the line Lowest the
libraries installed in a higher version than the lowest one their constraint allows.
dresscode config shows where the data starts and where raising the lower bound would lead. Versions marked
(reported) are already allowed by the constraint, so their changes are only reported:
Packages 3 with upgrading data
nette/application 3.2 upgrading/application.neon of dresscode/upgrade-nette, upgrading further to 3.2.1 (reported), 3.2.3 (reported), 3.2.9 (reported), 3.2.10 (reported), 3.3 (reported)
nette/component-model 3.2 upgrading/component-model.neon of dresscode/upgrade-nette, upgrading further to 4.0
nette/utils 4.1 upgrading/utils.neon of dresscode/upgrade-nette, upgrading further to 4.1.5 (reported)
One Major Version at a Time
Split a big jump into steps of one major version of a library. Every step is the same:
- In
composer.json, raise the lower bound to the next major version, for example from"nette/utils": "^3.2"to"^4.0". If you are also moving to a newer PHP, raiserequire.php, andconfig.platform.phptoo if the project sets it. - Run
composer update --prefer-lowest, so that vendor holds exactly the lowest versions the code is written for. - Run
dresscode upgrade, and if it advises so, run it once more. - Go through what is left, run the tests and PHPStan, read the diff and commit.
- Go on with the next version, or put the newest versions back into vendor with a plain
composer update.
Why --prefer-lowest? Where there is no data, DressCode rewrites by the @deprecated annotations in the
code of the library. But an annotation speaks only for the version that is installed. So DressCode does not use the annotations of
a library installed in a higher version than the lowest one the constraint allows: in the older version, the replacement might
not exist.
Why in steps? Because in each step, the data meets the very version of the library it describes. The new classes the data
writes into your code are already in vendor, and the @deprecated annotations of the installed version reveal what
will disappear in the next one; DressCode uses them to rewrite the code right away, or at least shows where you will need to
change it. How Upgrading Libraries
Works describes how to upgrade an application all at once, or rewrite the code before you update the library.
Keeping the Old API from Coming Back
upgrade is a one-time cleanup. To make every check in continuous integration and in a Git hook watch for the old API too, put the set deprecations with types
into the configuration, and name the package in use to let its rules in as well:
typeAnalysis: phpstan
use:
- perCs # your standard
- deprecations
- upgrade-nette
Packages of the vendor dresscode are written short in use, like built-in sets; the full name
dresscode/upgrade-nette works too. The data of a package applies even without its name in use; the name
adds the rules the package carries beside the data, for Nette, for example, the conversion of the flag Json::PRETTY
to a named argument. Where the name is missing, check and fix point it out once:
Warning: Package `dresscode/upgrade-nette` brings rules that run only where `use` names it, as `upgrade-nette`, and in `dresscode upgrade`; skipped, with what its upgrading files set for them.
From then on, dresscode check reports the old API in every new piece of code. And because the data follows the
constraints in composer.json, the check itself reports what needs rewriting as soon as you raise a constraint to the
next version.
Upgrading a Library You Maintain
A library that stands on other libraries is upgraded the same way as an application. When I upgraded
nette/application like this, upgrade rewrote setFactory() to setCreator() in
the code and in the tests, and the flag RouteList::ONE_WAY to the named argument oneWay: 1. It reported
a deprecated property and an annotation whose text does not fit into an attribute, and the second run rewrote nothing more.
But a library also has tests that use the old API on purpose: a test of a deprecated attribute, a test of backward
compatibility, a type test of an interface. You do not want those rewritten. Exclude such a file from the data with an override in
overrides:
typeAnalysis: phpstan
use:
- nette
- deprecations
overrides:
-
paths: [tests/UI/Requires.sameSite.handle.phpt]
upgrading:
packages: no
declarations:
deprecatedMember: keep
packages: no turns off, for that file, the data of the packages and the rewrites their files turn on, and
deprecatedMember: keep turns off the rewrite by @deprecated annotations; the style and the rest of the
set still apply. For a single line, the comment // dresscode:ignore upgrading is enough, see Suppressions. What upgrade still reports in such tests is fine: it
reports old API that is there on purpose.
The data does not rewrite the code of the class itself, because it describes how the class is used, not how it is implemented.
When the data of nette/application says that a write to $request->post should become
setPost($value), the line $this->post = $post inside the method Request::setPost()
stays, otherwise the method would call itself. A child class, though, is a user like any other and gets upgraded. Data about a
magic property, that is, about __get() and __set(), also applies to reading and writing a private
property from outside, because PHP sends that to __get() and __set() too.
Want to upgrade in parts, for example Nette first and PHP later? The option --only narrows the run to the data of
one package and the rules that read it:
dresscode upgrade --only upgrade-nette
The data of the other packages takes part only where another name in --only covers it, for example
deprecations or upgrading.api. You can go finer with the name of a library:
--only nette/forms takes only the files about nette/forms, whichever package carries them, and
--only php the data about what PHP has deprecated. check and fix support the same
--only, and the keys fixRisky and severity also accept the name of a package or a library.
Naming a package whose data does nothing here is an error, and the message says why: the data does not cover any version of the
project's libraries, or upgrading.packages is not yes. A name that is both an upgrade package and a
library another package writes about stands for both: the files of that package and the files about that library.
Options
| option | meaning |
|---|---|
--modernize |
also rewrites the code to newer PHP constructs, the set modernizations |
--ask-risky |
after the upgrade, asks about each remaining risky fix separately and shows its diff; only in an interactive terminal |
--fix-risky, --fix-risky=<name> |
applies the risky fixes of all decisions, or only of the named decision, section, preset, package or library |
--only <name> |
narrows the run to an upgrade package, a library, a decision, a section or a preset; can be given more than once |
--yes |
installs missing data without asking, even outside a terminal |
Otherwise upgrade accepts the options of fix: paths, --config, --use,
--set, --format, --jobs and others, see Command Line. It does not accept --diff.
Where to Go Next
- How upgrading libraries works, how DressCode knows what has changed, and what it rewrites by itself.
- Nette libraries, Symfony, Laravel and Dibi and Texy, what their packages cover.
- Upgrading to a newer PHP, when you raise the PHP version along with the libraries.
- Types from PHPStan, without which constants, methods and calls are not rewritten.