When a function or method modifies a by-reference parameter, PHPStan uses the @param-out PHPDoc tag to know the type of the variable after the call. But when the output type depends on the arguments passed to the call, a static PHPDoc is not sufficient.
The implementation is all about applying the core concepts so check out that guide first and then continue here.
This is the interface your extension needs to implement for non-static methods:
namespace PHPStan\Type;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
interface MethodParameterOutTypeExtension
{
public function isMethodSupported(
MethodReflection $methodReflection,
ParameterReflection $parameter,
): bool;
public function getParameterOutTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
ParameterReflection $parameter,
Scope $scope,
): ?Type;
}
The extension has two methods:
isMethodSupported()— returntruewhen your extension wants to handle the given method and parameter combination.getParameterOutTypeFromMethodCall()— return the output type of the by-reference parameter, ornullto fall back to the default behavior.
Here’s an example for a method that populates a by-reference parameter with a type determined by the first argument:
namespace App\PHPStan;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MethodParameterOutTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
class CastMethodParameterOutExtension implements MethodParameterOutTypeExtension
{
public function isMethodSupported(
MethodReflection $methodReflection,
ParameterReflection $parameter,
): bool
{
return $methodReflection->getDeclaringClass()->getName() === \App\Caster::class
&& $methodReflection->getName() === 'cast'
&& $parameter->getName() === 'value';
}
public function getParameterOutTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
ParameterReflection $parameter,
Scope $scope,
): ?Type
{
$args = $methodCall->getArgs();
if (count($args) < 1) {
return null;
}
$typeArg = $scope->getType($args[0]->value);
if ($typeArg instanceof ConstantStringType && $typeArg->getValue() === 'int') {
return new IntegerType();
}
return new StringType();
}
}
With this extension, PHPStan knows the type of the by-reference parameter after the call:
$caster = new Caster();
$caster->cast('int', $value);
\PHPStan\dumpType($value); // int
$caster->cast('string', $value);
\PHPStan\dumpType($value); // string
The implementation needs to be registered in your configuration file:
services:
-
class: App\PHPStan\CastMethodParameterOutExtension
tags:
- phpstan.methodParameterOutTypeExtension
There’s also analogous functionality for:
- static methods using
StaticMethodParameterOutTypeExtensioninterface andphpstan.staticMethodParameterOutTypeExtensionservice tag. - functions using
FunctionParameterOutTypeExtensioninterface andphpstan.functionParameterOutTypeExtensionservice tag.
Tracking dependencies for the result cache #
If the extension reads something PHPStan doesn’t know about to resolve the type, like a configuration file, typehint the $scope parameter as Scope&DependencyTracker in the PHPDoc and track it with $scope->trackFileDependency() or one of the other DependencyTracker methods. When it changes, the result cache analyses the file with the call again. Learn more »