PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs even before you write tests for the code.
PHPStan moves PHP closer to compiled languages in the sense that the correctness of each line of the code can be checked before you run the actual line.
It currently performs the following checks on your code:
- Existence of classes and interfaces in
instanceof
,catch
, typehints, other language constructs and even annotations. PHP does not do this and just stays silent instead. - Existence of variables while respecting scopes of branches and loops.
- Existence and visibility of called methods and functions.
- Existence and visibility of accessed properties.
- Correct number of parameters passed to constructors, methods and functions.
- Correct number of parameters passed to
sprintf
/printf
calls based on format strings.
Future versions will also check if arguments of correct types are passed to methods and functions and assigned to properties.
Unique feature of PHPStan is the ability to define and statically check "magic" behaviour of classes -
accessing properties that are not defined in the class but are created in __get
and __set
and invoking methods using __call
.
See Class reflection extensions and Dynamic return type extensions.
PHPStan requires PHP 7.0. You have to run it in environment with PHP 7 but the actual code does not have to use PHP 7 features. (Code written for PHP 5.6 and earlier can run on 7 mostly unmodified.)
PHPStan works best with modern object-oriented code. The more strongly-typed your code is, the more information you give PHPStan to work with.
Properly annotated and typehinted code (class properties, function and method arguments, return types) helps not only static analysis tools but also other people that work with the code to understand it.
To start performing analysis on your code, require PHPStan in Composer:
composer require phpstan/phpstan
Composer will install PHPStan's executable in its bin-dir
which defaults to vendor/bin
.
To let PHPStan analyse your codebase, you have use the analyse
commmand and point it to the right directories.
So for example if you have your classes in directories src
and tests
, you can run PHPStan like this:
vendor/bin/phpstan analyse src tests
PHPStan will probably find some errors, but don't worry, your code might be just fine. Errors found on the first run tend to be:
- Extra arguments passed to functions (e. g. function requires two arguments, the code passes three)
- Extra arguments passed to print/sprintf functions (e. g. format string contains one placeholder, the code passes two values to replace)
- Obvious errors in dead code
- Magic behaviour that needs to be defined. See Extensibility.
After fixing the obvious mistakes in the code, look to the following section for all the configuration options that will bring the number of reported errors to zero making PHPStan suitable to run as part of your continous integration script.
Config file is passed to the phpstan
executable with -c
option:
vendor/bin/phpstan analyse -c phpstan.neon src tests
NEON file format is very similar to YAML.
All the following options are part of the parameters
section.
PHPStan uses Composer autoloader so the easiest way how to autoload classes
is through the autoload
/autoload-dev
sections in composer.json.
If PHPStan complains about some nonexistent classes and you're sure the classes
exist in the codebase AND you don't want to use Composer autoloader for some reason,
you can specify directories to scan and concrete files to include using
autoload_directories
and autoload_files
array parameters:
parameters:
autoload_directories:
- %rootDir%/../../../build
autoload_files:
- %rootDir%/../../../generated/routes/GeneratedRouteList.php
%rootDir%
is expanded to the root directory where PHPStan resides.
If your codebase contains some files that are broken on purpose
(e. g. to test behaviour of your application on files with invalid PHP code),
you can exclude them using the excludes_analyse
array parameter. String at each line
is used as a pattern for the fnmatch
function.
parameters:
excludes_analyse:
- %rootDir%/tests/*/data/*
Classes without predefined structure are common in PHP applications.
They are used as universal holders of data - any property can be set and read on them. Notable examples
include stdClass
, SimpleXMLElement
, objects with results of database queries etc.
Use universalObjectCratesClasses
array parameter to let PHPStan know which classes
with these characteristics are used in your codebase:
parameters:
universalObjectCratesClasses:
- stdClass
- SimpleXMLElement
- Dibi\Row
- Ratchet\ConnectionInterface
If you use the initial assignment variable after for-loop or while-loop, set polluteScopeWithLoopInitialAssignments
boolean parameter to true
.
for ($i = 0; $i < count($list); $i++) {
// ...
}
echo $i;
If you use some variables from a try block in your catch blocks, set polluteCatchScopeWithTryAssignments
boolean parameter to true
.
try {
$author = $this->getLoggedInUser();
$post = $this->postRepository->getById($id);
} catch (PostNotFoundException $e) {
// $author is probably defined here
throw new ArticleByAuthorCannotBePublished($author);
}
If you are enumerating over all possible situations in if-elseif branches and PHPStan complains about undefined variables after the conditions, you can either write an else branch with throwing an exception:
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
} else {
throw new ShouldNotHappenException();
}
doFoo($foo);
Or you can set defineVariablesWithoutDefaultBranch
boolean parameter to true
and leave the code like this:
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
}
doFoo($foo);
I recommend leaving polluteScopeWithForLoopInitialAssignments
, polluteCatchScopeWithTryAssignments
and
defineVariablesWithoutDefaultBranch
set to false
because it leads to a clearer and more maintainable code.
Previous example showed that if a condition branches end with throwing an exception, that branch does not have to define a variable used after the condition branches end.
But exceptions are not the only way how to terminate execution of a method early. Some specific method calls
can be perceived by project developers also as early terminating - like a redirect()
that stops execution
by throwing an internal exception.
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
} else {
$this->redirect('homepage');
}
doFoo($foo);
These methods can be configured by specifying a class on whose instance they are called like this:
parameters:
earlyTerminatingMethodCalls:
Nette\Application\UI\Presenter:
- redirect
- redirectUrl
- sendJson
- sendResponse
If some issue in your code base is not easy to fix or just simply want to deal with it later, you can exclude error messages from the analysis result with regular expressions:
parameters:
ignoreErrors:
- '#Call to an undefined method [a-zA-Z0-9\\_]+::method\(\)#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::expects\(\)#'
- '#Access to an undefined property PHPUnit_Framework_MockObject_MockObject::\$[a-zA-Z0-9_]+#'
- '#Call to an undefined method PHPUnit_Framework_MockObject_MockObject::[a-zA-Z0-9_]+\(\)#'
If some of the patterns do not occur in the result anymore, PHPStan will let you know and you will have to remove the pattern from the configuration.
Classes in PHP can expose "magical" properties and methods decided in run-time using
class methods like __get
, __set
and __call
. Because PHPStan is all about static analysis
(testing code for errors without running it), it has to know about those properties and methods beforehand.
When PHPStan stumbles upon a property or a method that is unknown to built-in class reflection, it iterates over all registered class reflection extensions until it finds one that defines the property or method.
This extension type must implement the following interface:
namespace PHPStan\Reflection;
interface PropertiesClassReflectionExtension
{
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool;
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection;
}
Most likely you will also have to implement a new PropertyReflection
class:
namespace PHPStan\Reflection;
interface PropertyReflection
{
public function getType(): Type;
public function getDeclaringClass(): ClassReflection;
public function isStatic(): bool;
public function isPrivate(): bool;
public function isPublic(): bool;
}
This is how you register the extension in project's PHPStan config file:
services:
-
class: App\PHPStan\PropertiesFromAnnotationsClassReflectionExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
This extension type must implement the following interface:
namespace PHPStan\Reflection;
interface MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool;
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection;
}
Most likely you will also have to implement a new MethodReflection
class:
namespace PHPStan\Reflection;
interface MethodReflection
{
public function getDeclaringClass(): ClassReflection;
public function isStatic(): bool;
public function isPrivate(): bool;
public function isPublic(): bool;
public function getName(): string;
/**
* @return \PHPStan\Reflection\ParameterReflection[]
*/
public function getParameters(): array;
public function isVariadic(): bool;
public function getReturnType(): Type;
}
This is how you register the extension in project's PHPStan config file:
services:
-
class: App\PHPStan\EnumMethodsClassReflectionExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
If the return type of a method is not always the same, but depends on an argument passed to the method, you can specify the return type by writing and registering an extension.
Because you have to write the code with the type-resolving logic, it can be as complex as you want.
After writing the sample extension, the variable $mergedArticle
will have the correct type:
$mergedArticle = $this->entityManager->merge($article);
// $mergedArticle will have the same type as $article
This is the interface for dynamic return type extension:
namespace PHPStan\Type;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
interface DynamicMethodReturnTypeExtension
{
public static function getClass(): string;
public function isMethodSupported(MethodReflection $methodReflection): bool;
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type;
}
And this is how you'd write the extension that correctly resolves the EntityManager::merge() return type:
public static function getClass(): string
{
return \Doctrine\ORM\EntityManager::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'merge';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
if (count($methodCall->args) === 0) {
return $methodReflection->getReturnType();
}
$arg = $methodCall->args[0]->value;
$type = $scope->getType($arg);
if ($type->getClass() !== null) {
return $type;
}
return $methodReflection->getReturnType();
}
And finally, register the extension to PHPStan in the project's config file:
services:
-
class: App\PHPStan\EntityManagerDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
- If
include
orrequire
are used in the analysed code (instead ofinclude_once
orrequire_once
), PHPStan will throwCannot redeclare class
error. Use the_once
variants to avoid this error.
This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you are expected to uphold this code.