PHP at Scale #20
Welcome to the 20th edition of PHP at Scale. I am diving deep into the principles, best practices and practical lessons learned from scaling PHP projects - not only performance-wise but also code quality and maintainability.
I teased a CI/CD release back in issue #3 and kept pushing it to “next month”. This month, a couple of things happened that told me - now is the time to handle the CI/CD topic.
The thing with CI/CD is that I know a lot of teams with excellent pipelines, including very complex build and test steps. This is easy to check by browsing some popular, well-maintained open-source projects. Building and testing against multiple different environments (PHP versions, Database engines) is very common.
But let’s be honest - this is not the case for closed-source PHP projects. You need to focus on other things, make the pipeline as fast as possible, but also make sure it will catch issues early.
A second topic I would like to cover today, related to CI/CD, is vendor security and supply chain attacks, as recently CVE-2026-40176 and CVE-2026-40261 were disclosed.
PHP Ecosystem News
Announcing Plans for a PHP Ecosystem Survey and Report - No Survey yet, but you can suggest questions that should appear in it. I look forward to the Survey and even more to the report!
PHPverse 2026 - JetBrains just announced the PHPVerse 2026. Last year was pretty interesting, so I am looking forward to this year’s edition.
Start simple, and start now
On April 9th, braintree/braintree_php v6.33.0 was released. Maintained by PayPal’s payment arm. I would expect proper engineering to be applied in a payment provider company. Yet, the version shipped with a literal syntax error.
An extra colon before const TINY = 0; in Version.php. The file didn’t parse.
A php lint would have rejected it in milliseconds. Instead, it reached every project that ran composer update until a community member filed issue #360.
The easiest way to cover this inside a CI/CD pipeline is to run a command like:
find src/ -name “*.php” -print0 | xargs -0 -P4 php -lOn my rather large project, it takes 200ms to finish. You can adjust the -P4 flag to match your CPU cores to make it even faster.
I know some tools can do a similar check, maybe faster, but this is a zero-dependency solution.
In parallel, you can run other linters: Symfony comes with a lint:twig and lint:container command. If you use yaml, or anything else that comes into your mind - this is the place to start.
In the same tier - fast, free, ships with the tooling you already have:
composer auditbelongs in your first pipeline too. It runs in seconds, cross-references your locked dependencies against the advisories database, and fails the build when a known-vulnerable package is in composer.lock. We’ll come back to it later in this issue, but the point is: it’s a one-line addition to your pipeline, and there is no reason not to have it from day one.
Such a simple approach is a great first step for a pipeline that can be later extended. It does not make sense to trigger a more time-consuming test suite or a code formatting check if the code is not linting.
So if you don’t have a pipeline yet, start with a simple one, and do it now.
Depending on the project, I usually onboard tools like PHPStan, Psalm, PHP-CS-Fixer, Deptrac (If you are familiar with all those tools, you can skip this link). The tool choice is a matter of taste, but I also adjust it to the project being developed. As I work with legacy projects, it is not always possible to onboard all the tools from day 1. It also does not always make sense to do that.
So, besides the linter, I usually do my best to have PHPStan on board. It has a baseline and level options. Start with the lowest level, and gradually increase it.
One thing worth knowing before you adopt PHPStan on a legacy project: the baseline. On a 7-year-old codebase, level 8 will report thousands of errors on day one - overwhelming and demoralising. The trick is to run:
phpstan analyse --generate-baseline, commit the file, and from that point on, PHPStan only fails the build on new errors. Old code stays grandfathered in and gets cleaned up opportunistically when someone touches the file.
Two CI jobs work well: one fails on new errors above the baseline, one tracks baseline size as a non-blocking trend so you can see the legacy debt shrinking over time.
Magic number detection, copy-paste detection - they don’t fit into all projects, and neither does Deptrac. I only use them when the project is in a specific state, and they help. On the other hand, PHP CS Fixer is almost always helpful, but it might be hard to add in a project that is not yet formatted according to any guidelines ;).
Popular tools
I did a quick scan of a couple of open source projects to check what tools are used in their pipelines.
No surprise: PHPUnit (or Pest), PHPStan, and PHP-CS-Fixer are used in most projects. Composer audit, Composer validate, Deptrac and Symfony lint:* commands are closely following them, also used in most projects.
Psalm, Rector, PHPArkitect, ComposerRequireChecker are also quite common, but not as common as the previous tools. This might give you an idea of what tools to try first.
Performance of your pipeline
Pipeline performance shouldn’t be your first focus, but once the checks are dialled in, it pays back fast. A pipeline change is a one-time investment that runs hundreds - sometimes thousands - of times before you touch it again. Saving 30 seconds per run on a pipeline that fires 50 times a week adds up to over 20 hours of recovered developer time per year.
The real cost isn’t compute, though - it’s human. A 20-minute pipeline is a pipeline developers stop waiting for. They context-switch to another task, and by the time the failure email lands, they’re already deep in something else. The feedback loop the pipeline was supposed to create is broken. Aim for under 10 minutes; under 5 is even better. The biggest wins usually come from caching composer install, using a pre-baked CI image, and running independent steps in parallel.
Some articles that might help you to achieve this:
Optimizing CI/CD Process of the PHP application - describes how to use caches, parallel pipeline steps and some other smaller optimizations that can bring immediate effects to your pipeline.
If your automated tests depend on a database, you can try to optimize that too:
Supercharging Laravel CI/CD Pipeline from 9 minutes to 2 minutes with pre-built MySQL images
Are two examples of how to optimize your database-dependent tests.
But I will be honest, it is better to have more unit tests that do not rely on the database, and in some scenarios provide an in-memory repository. This makes it all run way faster than any of the articles above could achieve.
Composer security advisories — CVE-2026-40176 and CVE-2026-40261
As mentioned in the opening, CVE-2026-40176 and CVE-2026-40261 were disclosed - a CVSS 8.8 command-injection flaw in Composer’s Perforce driver, triggerable by malicious package metadata from any Composer repository. Installing a seemingly innocent package could execute OS commands on your machine. Packagist proactively disabled Perforce metadata four days before disclosure; the fixes are in Composer 2.9.6 and the 2.2.27 LTS.
Contrary to some articles you can find online, using --prefer-dist is not enough, and you might still be vulnerable. The key is to update Composer. Thankfully, the attack is now also blocked on the Packagist level, but it is good to know at least the basic information to prevent similar cases in the future.
Composer 2.9 — The Bouncer in the Dependency Resolver
Composer 2.9 has introduced an upgrade in how it handles security advisory enforcement. Previously, it was required to use roave/security-advisories a vendor package that handles that. Now this can be fully handled by Composer itself.
Sebastian Bergmann dives deeply into the topic, explaining not only the changes but also what they mean in practice, and how you can alter the settings to your needs.
Strengthening PHP Supply Chain Security with a Transparency Log for Packagist.org
I’ve mentioned composer audit. What I haven’t covered is that everything composer audit doesn’t catch. CVE-2026-40261 is a good reminder: the attack vector was the package manager itself, not the packages. An audit wouldn’t have helped.
Layers your CI and config should have, in order of how cheap they are to add:
Keep Composer itself patched.
composer self-updatein CI, or pin to 2.9.6 / 2.2.27 LTS in your build image. Older versions are still vulnerable to CVE-2026-40261.Run
composer auditon every PR. Already covered above - and since Composer 2.9, advisory-flagged versions are blocked by default during resolution. Audit still earns its place because it catches already-locked packages when a new advisory drops later.composer installIn CI, never update. Review lockfile diffs on PRs - that’s where suspicious new transitive deps and surprise maintainer changes show up.
Beyond that - allow-plugins discipline, --no-scripts/--no-plugins in CI, private mirrors like Private Packagist or Packeton, Renovate for visible updates, and dependency-confusion protection - those are real, but they’re a topic for another issue. The three above are where everyone should start.
The linked Packagist blog post is about the emerging transparency log - an auditable public record of security-relevant events in the Packagist ecosystem (the PHP community’s pragmatic answer until cryptographic signing exists). Worth reading for where the ecosystem is heading. PHP doesn’t have package signing yet; the transparency log is the current direction.
Pipeline security and supply chain attacks are very popular in 2026, and are getting more and more common.
Two things I’d love to hear from you in the comments: what’s the cheapest CI check you’ve ever added that caught the most bugs? And - should I dedicate the next edition to supply chain security, or move on to a different topic?
PS. I have developed an AI-powered modernization advisor that includes knowledge from our blog posts to help you plan your dedicated PHP app modernization process. Available for free here, any feedback is more than welcome!
PS2. We help teams modernize PHP applications and fix the architectural problems that hold them back. If that sounds like your situation, we currently have a slot for a new project. Just email me or reach out on LinkedIn.
Why is this newsletter for me?
If you are passionate about well-crafted software products and despise poor software design, this newsletter is for you! With a focus on mature PHP usage, best practices, and effective tools, you'll gain valuable insights and techniques to enhance your PHP projects and keep your skills up to date.
I hope this edition of PHP at Scale is informative and inspiring. I aim to provide the tools and knowledge you need to excel in your PHP development journey. As always, I welcome your feedback and suggestions for future topics. Stay tuned for more insights, tips, and best practices in our upcoming issues.
May thy software be mature!


