# PHPStan Symfony Framework extensions and rules

[![Build](https://github.com/phpstan/phpstan-symfony/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-symfony/actions)
[![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-symfony/v/stable)](https://packagist.org/packages/phpstan/phpstan-symfony)
[![License](https://poser.pugx.org/phpstan/phpstan-symfony/license)](https://packagist.org/packages/phpstan/phpstan-symfony)

* [PHPStan](https://phpstan.org/)

This extension provides following features:

* Provides correct return type for `ContainerInterface::get()` and `::has()` methods.
* Provides correct return type for `Controller::get()` and `::has()` methods.
* Provides correct return type for `AbstractController::get()` and `::has()` methods.
* Provides correct return type for `ContainerInterface::getParameter()` and `::hasParameter()` methods.
* Provides correct return type for `ParameterBagInterface::get()` and `::has()` methods.
* Provides correct return type for `Controller::getParameter()` method.
* Provides correct return type for `AbstractController::getParameter()` method.
* Provides correct return type for `Request::getContent()` method based on the `$asResource` parameter.
* Provides correct return type for `HeaderBag::get()` method based on the `$first` parameter.
* Provides correct return type for `Envelope::all()` method based on the `$stampFqcn` parameter.
* Provides correct return type for `InputBag::get()` method based on the `$default` parameter.
* Provides correct return type for `InputBag::all()` method based on the `$key` parameter.
* Provides correct return type for `ResponseHeaderBag::getCookies()` method based on the format argument.
* Provides correct return types for `TreeBuilder` and `NodeDefinition` objects.
* Provides correct return type for `SerializerInterface::deserialize()` and `DenormalizerInterface::denormalize()` methods based on the `$type` argument.
* Provides correct return type for `HandleTrait::handle()` method based on the message handler map.
* Provides correct return type for `FormInterface::getErrors()` method based on the `$deep` and `$flatten` parameters.
* Provides correct return type for `KernelInterface::locateResource()` method.
* Provides correct return type for `Extension::getConfiguration()` method.
* Provides correct return type for `CacheInterface::get()` method based on the callback return type.
* Provides correct return type for `BrowserKitAssertionsTrait::getClient()` method.
* Provides configurable return type resolution for methods that internally use Messenger `HandleTrait`.
* Notifies you when you try to get an unregistered service from the container.
* Notifies you when you try to get a private service from the container.
* Notifies you when you access undefined console command arguments or options.
* Validates default values of console command arguments and options.
* Recognizes `@required` annotation and `#[Required]` attribute for autowiring properties and methods.
* Optionally correct return types for `InputInterface::getArgument()`, `::getOption()`, `::getOptions()`, `::hasArgument()`, and `::hasOption()`.
* Optionally provides correct return type for `Command::getHelper()` method.


## Installation

To use this extension, require it in [Composer](https://getcomposer.org/):

```
composer require --dev phpstan/phpstan-symfony
```

If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set!

<details>
  <summary>Manual installation</summary>

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
```
</details>

# Configuration

You have to provide a path to `srcDevDebugProjectContainer.xml` or similar XML file describing your container.

```yaml
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
```

## Constant hassers

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:

```php
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.

## Analysis of Symfony Console Commands

You can opt in for more advanced analysis of [Symfony Console Commands](https://symfony.com/doc/current/console.html)
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()`.

```neon
parameters:
	symfony:
		consoleApplicationLoader: tests/console-application.php
```

Symfony 4:

```php
// 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:

```php
// 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);
```

[Single Command Application](https://symfony.com/doc/current/components/console/single_command_tool.html):

```php
// 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`:

```yaml
parameters:
    container.dumper.inline_class_loader: false
```

Call the new env in your `console-application.php`:

```php
$kernel = new \App\Kernel('phpstan_env', (bool) $_SERVER['APP_DEBUG']);
```

## Messenger HandleTrait Wrappers

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.

### Configuration

```neon
parameters:
    symfony:
        messenger:
            handleTraitWrappers:
                - App\Bus\QueryBus::dispatch
                - App\Bus\QueryBus::execute
                - App\Bus\QueryBusInterface::dispatch
```

### Message Handlers

```php
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);
    }
}
```

### PHP Examples

```php
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'.
```
