phpstan/phpstan-symfony
PHPStan extension for Symfony that improves static analysis with precise return types and framework-specific rules. Understands container/services, parameters, controllers, request/headers, serializer, forms, messenger handlers, cache callbacks, config tree builders, and more.
This extension provides following features:
ContainerInterface::get() and ::has() methods.Controller::get() and ::has() methods.AbstractController::get() and ::has() methods.ContainerInterface::getParameter() and ::hasParameter() methods.ParameterBagInterface::get() and ::has() methods.Controller::getParameter() method.AbstractController::getParameter() method.Request::getContent() method based on the $asResource parameter.HeaderBag::get() method based on the $first parameter.Envelope::all() method based on the $stampFqcn parameter.InputBag::get() method based on the $default parameter.InputBag::all() method based on the $key parameter.ResponseHeaderBag::getCookies() method based on the format argument.TreeBuilder and NodeDefinition objects.SerializerInterface::deserialize() and DenormalizerInterface::denormalize() methods based on the $type argument.HandleTrait::handle() method based on the message handler map.FormInterface::getErrors() method based on the $deep and $flatten parameters.KernelInterface::locateResource() method.Extension::getConfiguration() method.CacheInterface::get() method based on the callback return type.BrowserKitAssertionsTrait::getClient() method.HandleTrait.@required annotation and #[Required] attribute for autowiring properties and methods.InputInterface::getArgument(), ::getOption(), ::getOptions(), ::hasArgument(), and ::hasOption().Command::getHelper() method.To use this extension, require it in Composer:
composer require --dev phpstan/phpstan-symfony
If you also install phpstan/extension-installer then you're all set!
If you don't want to use phpstan/extension-installer, include extension.neon in your project's PHPStan config:
includes:
- vendor/phpstan/phpstan-symfony/extension.neon
To perform framework-specific checks, include also this file:
includes:
- vendor/phpstan/phpstan-symfony/rules.neon
You have to provide a path to srcDevDebugProjectContainer.xml or similar XML file describing your container.
parameters:
symfony:
containerXmlPath: var/cache/dev/srcDevDebugProjectContainer.xml
# or with Symfony 4.2+
containerXmlPath: var/cache/dev/srcApp_KernelDevDebugContainer.xml
# or with Symfony 5+
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
# If you're using PHP config files for Symfony 5.3+, you also need this for auto-loading of `Symfony\Config`:
scanDirectories:
- var/cache/dev/Symfony/Config
# If you're using PHP config files (including the ones under packages/*.php) for Symfony 5.3+,
# you need this to load the helper functions (i.e. service(), env()):
scanFiles:
- vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php
Sometimes, when you are dealing with optional dependencies, the ::has() methods can cause problems. For example, the following construct would complain that the condition is always either on or off, depending on whether you have the dependency for service installed:
if ($this->has('service')) {
// ...
}
In that case, you can disable the ::has() method return type resolving like this:
parameters:
symfony:
constantHassers: false
Be aware that it may hide genuine errors in your application.
You can opt in for more advanced analysis of Symfony Console Commands
by providing the console application from your own application. This will allow the correct argument and option types to be inferred when accessing $input->getArgument() or $input->getOption().
parameters:
symfony:
consoleApplicationLoader: tests/console-application.php
Symfony 4:
// tests/console-application.php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
require __DIR__ . '/../config/bootstrap.php';
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
return new Application($kernel);
Symfony 5:
// tests/console-application.php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Dotenv\Dotenv;
require __DIR__ . '/../vendor/autoload.php';
(new Dotenv())->bootEnv(__DIR__ . '/../.env');
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
return new Application($kernel);
// tests/console-application.php
use App\Application; // where Application extends Symfony\Component\Console\SingleCommandApplication
use Symfony\Component\Console;
require __DIR__ . '/../vendor/autoload.php';
$application = new Console\Application();
$application->add(new Application());
return $application;
You may then encounter an error with PhpParser:
Compile Error: Cannot Declare interface PhpParser\NodeVisitor, because the name is already in use
If this is the case, you should create a new environment for your application that will disable inlining. In config/packages/phpstan_env/parameters.yaml:
parameters:
container.dumper.inline_class_loader: false
Call the new env in your console-application.php:
$kernel = new \App\Kernel('phpstan_env', (bool) $_SERVER['APP_DEBUG']);
The extension provides advanced type inference for methods that internally use Symfony Messenger's HandleTrait. This feature is particularly useful for query bus implementations (in CQRS pattern) that use/wrap the HandleTrait::handle() method.
parameters:
symfony:
messenger:
handleTraitWrappers:
- App\Bus\QueryBus::dispatch
- App\Bus\QueryBus::execute
- App\Bus\QueryBusInterface::dispatch
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
// Product handler that returns Product
#[AsMessageHandler]
class GetProductQueryHandler
{
public function __invoke(GetProductQuery $query): Product
{
return $this->productRepository->get($query->productId);
}
}
use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;
// Basic query bus implementation
class QueryBus
{
use HandleTrait;
public function __construct(MessageBusInterface $messageBus)
{
$this->messageBus = $messageBus;
}
public function dispatch(object $query): mixed
{
return $this->handle($query); // Return type will be inferred in calling code as query result
}
// Multiple methods per class example
public function execute(object $message): mixed
{
return $this->handle($message); // Return type will be inferred in calling code as query result
}
}
// Interface-based configuration example
interface QueryBusInterface
{
public function dispatch(object $query): mixed; // Return type will be inferred in calling code as query result
}
class QueryBusWithInterface implements QueryBusInterface
{
use HandleTrait;
public function __construct(MessageBusInterface $queryBus)
{
$this->messageBus = $queryBus;
}
public function dispatch(object $query): mixed
{
return $this->handle($query);
}
}
// Examples of use with proper type inference
$query = new GetProductQuery($productId);
$queryBus = new QueryBus($messageBus);
$queryBusWithInterface = new QueryBusWithInterface($messageBus);
$product = $queryBus->dispatch($query); // Returns: Product
$product2 = $queryBus->execute($query); // Returns: Product
$product3 = $queryBusWithInterface->dispatch($query); // Returns: Product
// Without the feature all above query bus results would be default 'mixed'.
How can I help you explore Laravel packages today?