PHPStan for Magento 2: Static Analysis Setup Guide
Magento code is full of places where a type can be wrong without anything failing until production. A repository method that returns an interface, a collection item that could be null, a config value that is a string when you expect an integer. PHP itself only notices when the line runs. PHPStan reads the code without running it and reports those problems before they ship.
Running PHPStan on Magento used to be frustrating, mostly because of generated classes and magic methods. With the right extension and a sensible starting point, it is now a normal part of a Magento pipeline. This article shows how I set it up on 2.4.8 projects.
What PHPStan finds
Typical findings in Magento custom code:
- Calling a method on a value that can be null, for example the result of a lookup that returns an item or null
- Calling a method that does not exist on the declared type, often after a refactor
- Passing a string where an int is declared, for example a store ID from a request parameter
- Return types that do not match the declared return type
- Unused private properties and methods, dead branches and always true conditions
- Wrong arguments to core methods after an upgrade changed their signature
None of these are style issues. They are bugs or future bugs.
Installation
Install PHPStan and the Magento extension as dev dependencies:
composer require --dev phpstan/phpstan bitexpert/phpstan-magento phpstan/extension-installer
phpstan/extension-installer registers installed extensions automatically, so you do not have to include their config files by hand. Composer will ask you to allow it as a plugin.
The bitexpert/phpstan-magento extension is what makes PHPStan usable on Magento. Among other things, it knows how to resolve the classes Magento generates at runtime, such as factories and proxies, so PHPStan does not report them as missing classes. It also understands extension attribute interfaces. Check the extension's README for the full list of features in the version you install.
Configuration
Create phpstan.neon in the project root:
parameters:
level: 5
paths:
- app/code
excludePaths:
- app/code/*/*/Test/*
tmpDir: var/phpstan
Analyse your own code only. PHPStan will still read core and vendor classes to know their types, it just will not report errors in them. Whether to analyse tests depends on your team. Tests with many mocks generate a lot of noise at higher levels, so many projects exclude them at first and add them later.
Run it:
vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=2G
Magento is a large codebase to read, so the default memory limit is often too low. Run it inside the PHP container of your local environment, so it sees the same PHP version and extensions as the application.
Choosing a level
PHPStan levels go from 0, which checks only basic things like unknown classes, to the highest levels, which check strict null handling, mixed types and more. The levels in short:
- Low levels find unknown classes and methods, wrong argument counts and undefined variables. Every project should pass these.
- Middle levels add return types, argument types, dead code and possibly undefined variables. This is a good target for most Magento projects.
- High levels add strict checks on nullable values, union types and
mixed. Very useful on new code, but Magento core returns loosely typed values in many places, so expect friction.
The practical approach: run each level from low to high and stop when the error count becomes more than the team can fix in a reasonable time. Set that as the level, add a baseline, and raise the level later.
Baselines for existing code
On a project with years of custom code, the first run may report hundreds of errors. A baseline records the current errors so PHPStan ignores them and reports only new ones:
vendor/bin/phpstan analyse -c phpstan.neon --generate-baseline
This writes phpstan-baseline.neon. Include it from the main config:
includes:
- phpstan-baseline.neon
parameters:
level: 6
paths:
- app/code
tmpDir: var/phpstan
Commit the baseline. From now on, CI fails only on errors that are not in it. When someone fixes a baseline error, PHPStan reports that the ignored error no longer occurs, so the baseline has to be regenerated. That is a good thing, because it keeps the file shrinking over time.
Common Magento issues and how to fix them
Nullable results
public function getBrandLabel(int $productId): string
{
$product = $this->findProduct($productId);
return (string) $product->getCustomAttribute('brand')?->getValue();
}
If findProduct() is declared to return ?ProductInterface, PHPStan reports the method call on a possibly null value at the levels that check nullability. The fix is to handle the null case explicitly, usually with an early return or an exception, instead of hoping it never happens.
Magic getters on models
Models extending DataObject answer to getAnything() through __call. PHPStan cannot know which of those return what. The cleanest fix is to use interface methods such as getSku() or getData('field') with an explicit cast, and to add real getters with return types to your own models and data interfaces.
Config values
ScopeConfigInterface::getValue() returns mixed. Cast the result to the type you need at the point where you read it, and do it in one small class per module that exposes typed methods like isEnabled(): bool. That removes casting from the rest of the code and gives PHPStan exact types.
Arrays without shapes
At higher levels PHPStan complains about arrays without a value type. Use docblocks like @return array<int, string> or @param array{sku: string, qty: float} $row. They document intent as much as they satisfy the tool.
Generated code and di:compile
The Magento extension resolves factories and proxies even when generated/ is empty. If you see errors about missing interceptors or other generated classes, check that the extension is installed and registered. Running setup:di:compile before PHPStan also works, but it is slow and should not be a requirement for a static analysis job.
Running in CI
PHPStan fits next to PHPCS and unit tests in the checks job of a pipeline, as in the GitHub Actions article. Two tips:
- Cache the
tmpDirbetween runs. PHPStan's result cache makes repeat runs much faster when few files changed. - Use
--error-format=githubon GitHub Actions so findings show up as annotations on the pull request diff.
vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=2G --no-progress --error-format=github
PHPStan and upgrades
Static analysis is especially useful during a Magento or PHP upgrade. When a core method changes its signature or a class is removed, PHPStan finds every call in custom code in seconds, without clicking through the store. Run it on the upgrade branch after composer update and treat new errors as the first part of the upgrade work. The PHP 8 compatibility article covers the PHP side of that.
Where PHPStan stops
PHPStan does not know about XML. A plugin declared for a method that does not exist, a wrong class in di.xml or a typo in an event name will not show up. setup:di:compile catches some of those, integration tests catch more. It also does not judge design or performance. Combine it with the Magento coding standard and a real code review.
FAQ
Why does PHPStan say a Factory class does not exist?
Factories, proxies and similar classes are generated by Magento and may not exist on disk when PHPStan runs. The bitexpert/phpstan-magento extension teaches PHPStan about them, so install it instead of excluding those errors.
Which PHPStan level should a Magento project use?
Start at the highest level your code passes with a manageable baseline, often somewhere in the middle. Raise it one level at a time. New modules can usually use a stricter level than old ones.
Should PHPStan analyse vendor/magento?
No. Analyse your own code in app/code and your own packages. PHPStan still reads core classes to understand types, it just does not report errors in them.
What is the difference between PHPStan and PHPCS?
PHPCS checks how code is written: formatting, naming and forbidden patterns. PHPStan checks whether the code is correct: types, method existence, null handling and dead code. They complement each other.
I am available for freelance Magento and Adobe Commerce work, including code quality setups for existing projects. If you need help with this, get in touch.
Related articles
AI Tools for Magento Development: What Works, What Fails
Using AI coding assistants on Magento 2 projects: where they save time, where they invent code, how to give them context and how to verify their output.
Magento 2 Coding Standard with PHPCS: Setup and Usage
Set up the Magento 2 coding standard with PHP_CodeSniffer: the Magento2 ruleset, a project phpcs.xml, severity levels, phpcbf and checking changed files.
Common Magento 2 Errors and How to Fix Them
Fixes for classic Magento 2 errors: Area code is not set, Class does not exist, Invalid Form Key, stuck indexers, Unable to unserialize value, static 404s.
Magento 2 GraphQL: Add a Custom Query and Resolver
How to add a custom GraphQL query in Magento 2.4: schema.graphqls, a resolver with ResolverInterface, errors, auth checks and cache identity for Varnish.