Maggie 3.0Meet the newly updated AI tutor

See what's new
Static Analysis in Magento with PHPStan

Static Analysis in Magento with PHPStan

Learn about PHPStan and a related Magento plugin, which provide the ability to help follow Magento best practices by analyzing code during development.

To run PHPStan on Magento 2.4.9, install PHPStan 2 with bitexpert/phpstan-magento and phpstan/extension-installer, overriding Magento's own ^1.9 pin. Then run vendor/bin/phpstan analyse app/code at a level from 0 to 10. The Magento extension teaches PHPStan about factories, proxies and magic methods, so the errors you get are real ones.

Magento already ships PHPStan as a dev dependency. It's just the wrong major version for the Magento extension you actually want, and the obvious composer require quietly installs a plugin from 2021. Here's how to get it right.

What is PHPStan?

PHPStan is a static analyzer for PHP. A static analyzer checks your code for errors without executing it, so you catch bugs while you're writing the code instead of when it runs.

PHPStan logo

You've probably already met the other big code checker, PHP_CodeSniffer. Magento 2.4.9 ships magento/magento-coding-standard in its require-dev, which registers the Magento2 PHPCS standard, and setting up PHPCS is still very useful for making sure code follows Magento's formatting and coding rules.

PHPCS only looks at one file at a time, though. It can't trace a call back to a parent class or check that the type you pass actually matches the type a method expects. Given how dynamic PHP is (especially in a codebase as large as Magento), you want that deeper analysis too, and that's PHPStan's job.

Why does composer refuse to install bitexpert/phpstan-magento?

Check the root composer.json of a Magento 2.4.9 install and you'll find this in require-dev:

"phpstan/phpstan": "^1.9",

The lock file resolves it to PHPStan 1.12. Confirm what you're actually running with:

composer show phpstan/phpstan

Meanwhile, PHPStan 2.0 shipped at the end of 2024, and the current Magento extension, bitexpert/phpstan-magento 0.43 and newer, requires phpstan/phpstan: ^2.0.

So if you just run the obvious command:

composer require --dev bitexpert/phpstan-magento

Composer doesn't fail. It goes looking for an old release that still works with PHPStan 1, and on a stock 2.4.9 project it lands on bitexpert/phpstan-magento v0.11.0, from November 2021. Every newer release that still supports PHPStan 1 requires an older symfony/finder or laminas/laminas-code than the ones 2.4.9 ships with, and v0.11.0 is the last one that doesn't constrain either.

The obvious fix doesn't work either:

composer require --dev phpstan/phpstan:^2.0 bitexpert/phpstan-magento phpstan/extension-installer --with-all-dependencies

This one fails, and the reason is a package you never asked for. magento/magento-coding-standard requires rector/rector, the lock file has Rector 1.2.10, and Rector 1.2.10 requires PHPStan ^1.12.5. Rector 2 works with PHPStan 2, but Composer won't upgrade it unless you name it in the update.

Installing PHPStan 2 and the Magento extension on 2.4.9

Here's the install path that actually works on a stock Magento 2.4.9 project. First, allow the extension installer as a Composer plugin, so Composer doesn't stop to ask:

composer config allow-plugins.phpstan/extension-installer true

Then write the new constraints to composer.json without installing anything yet. This replaces Magento's ^1.9 line with ^2.0:

composer require --dev --no-update phpstan/phpstan:^2.0 bitexpert/phpstan-magento:^0.44 phpstan/extension-installer:^1.4

Finally, update those packages along with Rector:

composer update phpstan/phpstan rector/rector bitexpert/phpstan-magento phpstan/extension-installer --with-all-dependencies

On 2.4.9 that upgrades PHPStan from 1.12 to 2.3, Rector from 1.2 to 2.x, and installs bitexpert/phpstan-magento 0.44 and the extension installer. Your composer.lock pins those exact versions, so nothing bumps without you knowing about it.

A few things worth knowing about this:

  • You're overriding a Magento pin. If you later run composer require-commerce or the root update plugin for an upgrade, check that it didn't put ^1.9 back.
  • Core's own PHPStan test was written for 1.x. Magento ships its own config at dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon for its static test suite. If you run Magento's static tests in CI, make sure they still pass after the upgrade.
  • Dev dependencies stay off production, but only if your production deploys run composer install --no-dev. If they don't, all of this ships to prod too.

What the Magento extension does

Stephan Hochdörfer of bitExpert has been a long-time Magento developer, and he's the lead author of bitexpert/phpstan-magento. Thanks Stephan!

bitexpert/phpstan-magento GitHub repo

Without it, PHPStan drowns in false positives on Magento code. It doesn't know that ProductFactory or Session\Proxy classes get generated at runtime, and it doesn't understand the magic getters and setters on DataObject. The extension fills in those gaps.

The PHPStan Extension Installer is what registers the extension with PHPStan. It's a Composer plugin that finds installed PHPStan extensions and loads their config automatically, so you don't have to include anything by hand.

If you skipped the composer config step above, Composer asks you to trust the plugin during install:

Composer allow-plugins prompt

Type y to continue. Either way, you end up with "phpstan/extension-installer": true in the allow-plugins section of composer.json.

Running PHPStan on Magento code

With everything installed, run PHPStan against a directory:

vendor/bin/phpstan analyse app/code/Vendor/Module

Rather than typing the level and paths every time, drop a phpstan.neon in the Magento root. PHPStan picks it up automatically:

parameters:
    level: 5
    paths:
        - app/code

If you run PHPStan from somewhere other than the Magento root, the extension also needs to know where Magento lives:

parameters:
    magento:
        magentoRoot: /var/www/html

Which PHPStan level should I use for Magento?

PHPStan's strictness is controlled by rule levels. Level 0 is the most forgiving, and PHPStan 2 added level 10, so the range is now 0 to 10. You can set it in phpstan.neon or override it on the command line:

vendor/bin/phpstan analyse --level 10 app/code/Vendor/Module

To give you a feel for the difference, I pointed PHPStan 2.3 with the Magento extension at core's own Magento_Contact module on 2.4.9 (unit tests included). Level 0 came back clean. Level 5 found 5 errors. Level 10 found 84.

My take: for a new module, start high and stay there, because it's far easier to keep code clean than to clean it up later. For an existing codebase, pick the level where it mostly passes, then generate a baseline so PHPStan only complains about new problems:

vendor/bin/phpstan analyse --generate-baseline

That writes a phpstan-baseline.neon you include from phpstan.neon. Fix the old errors over time, then raise the level.

Where static analysis sits next to tests

PHPStan tells you the code is consistent: types line up, methods exist, nothing is called on a possible null. It can't tell you the code does the right thing. That's what tests are for, and Magento 2.4.9 ships the tooling for each layer in its require-dev:

  • Unit tests run on PHPUnit (12.x on 2.4.9) and test one class in isolation with its dependencies mocked. They're fast, so run them on every commit.
  • Integration tests live in dev/tests/integration and run your code against a real Magento install and database. They catch DI configuration, plugin and database problems that unit tests mock away.
  • MFTF (Magento Functional Testing Framework, run with vendor/bin/mftf) drives a real browser through the storefront and admin. MFTF tests are the slowest of the bunch, so save them for critical flows like checkout.

Static analysis is the cheapest of all of these. It runs in seconds, needs no database, and catches a whole class of bugs before a single test executes, so it makes sense to run it first.

Taking it further

Once PHPStan runs locally, the next step is running it in your deployment pipeline so every pull request gets checked. And if you're up for it, you can write custom PHPStan rules or an extension specific to your own codebase, just as Stephan did.

PHPStan is a lot more useful on code that's typed end to end, and the repository pattern refactor is a good example of getting a module there. And if you've found a PHPStan level or rule that works especially well on Magento code, I'd like to hear about it.

Something here out of date?

Magento moves, and so does this site. If a step no longer works, tell me and I'll fix it.

Get the next one by email

Every other week I send a short Magento newsletter: release news, ecosystem news, a question for you, and a take of my own. Enroll in the Library and it's yours. No credit card needed.

The University includes every Magento course available, and every new M.academy course as it ships.