Welcome to this new article where I will show you and explain how I update my PHP package on a Laravel app weekly.
Summary
Why upgrading periodically
Two main reasons come to my mind.
First, to keep my app free of any possible bugs. Since I will rely on a dozens of packages, and I choose the packages that are most likely to be maintained over time, it may happen the maintainer found a performance or feature issue and has fixed it already.
Second, this helps me do major versions upgrade easily since I would have done most of the migration path (upgrading to the latest minor version), which may have already introduced deprecations to follow to ease a major version upgrade.
My versioning policy
When I work on an app that is "closed source" (like Predzo), I lock every packages to their exact version.
By that I mean instead of installing a package like this
composer require google/recaptcha
I will force Composer to install an exact version
composer require google/recaptcha:1.3.0
I had some situations when a package would introduce a breaking change as a minor/fix update (even had one time occuring on a Laravel minor change). Since then, I got used to just lock the exact version in order to be in peace when installing the same dependencies on my production server.
I also do the same for NPM packages
npm install --save-dev --save-exact @sentry/browser@7.48.0
I know this will not prevent dependencies of my direct dependencies from being updated on another version (since my direct dependencies do not freeze their own dependencies to the exact versions), but at least it helps mitigate this issue on direct dependencies.
Also, since I work in a "continuous deploy" manner, features that I finish will never last more than a day or two before being pushed on production. This limits the risks of having too much differences.
The setup
I rely on a Github Action to run the command composer outdated --strict --direct
on a CI. This means whenever I complete a ticket and I am ready to push it, if the dependencies are not up-to-date, my merge request will not pass the tests.
Composer
{
"scripts": {
"check-updates": "composer outdated --strict --direct"
}
}
composer run check-updates
The --strict
option will make the command to return a non-zero code to the console if at least one dependency is outdated.
The --direct
option instructs Composer to only check on direct dependencies (the ones in composer.json
). Without this option, it would also check on dependencies of my direct dependencies.
NPM
{
"scripts": {
"check-updates": "npm outdated"
}
}
npm run check-updates
Out of the box, npm outdated
returns a non-zero code by default if at least one dependency is outdated. It also only scans direct dependencies by default.
When some dependencies are outdated, this is my routine:
- I open the repository on Github
- I read the changelog
- I copy the latest version from the command output
So for example if jawira/case-converter
, league/flysystem-aws-s3-v3
and laravel/cashier
are outdated, I will run this command:
composer require jawira/case-converter:3.5.1 league/flysystem-aws-s3-v3:3.13.0 laravel/cashier:14.11.0
Finally, I run my tests to ensure nothing have been broken in the process of upgrading dependencies.
This whole process ensures everytime I will work on features, if dependencies are not up-to-date, I will also take the opportunity to update my dependencies.
Conclusion
As you can imagine, I never run composer update
. I always check the changelog of my dependencies, and install the exact version. This has multiple benefits:
- I force myself to actually read the changelogs, most of the times there is precious information on deprecations upgrade guides or new interesting undocumented features
- I do not accidentally upgrade a dependency that could potentially introduce a breaking change
- There is a satisfying feeling when
composer outdated
returns 0 outdated dependencies - I stay up-to-date weekly (or sometimes several times per weeks if I have the occasion to work on Predzo more often)
One drawback to this is it relies on a strong code coverage and tested code base as you want to be sure nothing breaks when upgrading so often. I would not advice to do this on a project with a little test coverage.
Another one is I noticed upgrading major versions takes a bit more time (I did 2 majors upgrade on Predzo, from Laravel 8 to 9 and from Laravel 9 to 10). This is because I have to run a complete command including all the dependencies and their latest version at a single time. If I do not do it, Composer will tell me my version is too strict to upgrade packages.
However this is mitigated as I do not have too much to figure out since I normally upgrade packages to the latest minor versions possible so the gap is not that big most of the time.
So far, after 2 years working on my app and experimenting with this setup, I am pretty happy with it and I can confidently say I feel safer with this process in the sense I know my dependencies are the closest possible between my local and production environment.
To finish, I have done a poll on Twitter (follow me) and noticed most of the time people will choose to upgrade their dependencies only when a major version is released. I think it is a pity when we know how hard maintainers work to keep their packages performant and bug free. Starting to upgrade more often could lead in potentially safer and more secure products.
I hope this article gave you an interesting point of view regarding package upgrades! Let me know in the comment section what do you think about it.
Happy package upgrades!
Cover photo by Yulianto Poitier from Pexels.
Top comments (1)
Not that much tutorials show that you can install packages using a fixed version, that's a shame because many breaking changes can happen, even on minor or patched version, in times when changing a lot of code is not appropriate.
Very good advice, thanks!