A set of PHPStan rules to improve static analysis for Symfony UX applications.
To install the PHPStan rules for Symfony UX, you can use Composer:
composer require --dev kocal/phpstan-symfony-uxIf you have phpstan/extension-installer installed (which is the case by default), the extension will be automatically registered and you're ready to go.
If you don't use the extension installer, you'll need to manually add the extension to your phpstan.neon or phpstan.dist.neon configuration file:
includes:
- vendor/kocal/phpstan-symfony-ux/extension.neonEach rule can be enabled individually by adding it to your phpstan.neon or phpstan.dist.neon configuration file.
Enforces that all methods annotated with #[LiveAction] in LiveComponents must be declared as public.
LiveAction methods need to be publicly accessible to be invoked as component actions from the frontend.
rules:
- Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LiveActionMethodsShouldBePublicRule// src/Twig/Components/TodoList.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[AsLiveComponent]
final class TodoList
{
#[LiveAction]
private function addItem(): void
{
}
}// src/Twig/Components/TodoList.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[AsLiveComponent]
final class TodoList
{
#[LiveAction]
protected function deleteItem(): void
{
}
}β
// src/Twig/Components/TodoList.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[AsLiveComponent]
final class TodoList
{
#[LiveAction]
public function addItem(): void
{
}
#[LiveAction]
public function deleteItem(): void
{
}
}π
Enforces that all methods annotated with #[LiveListener] in LiveComponents must be declared as public.
LiveListener methods need to be publicly accessible to be invoked when listening to events from the frontend.
rules:
- Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LiveListenerMethodsShouldBePublicRule// src/Twig/Components/Notification.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
#[AsLiveComponent]
final class Notification
{
#[LiveListener('notification:received')]
private function onNotificationReceived(): void
{
}
#[LiveListener('notification:dismissed')]
protected function onNotificationDismissed(): void
{
}
}β
// src/Twig/Components/Notification.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
#[AsLiveComponent]
final class Notification
{
#[LiveListener('notification:received')]
public function onNotificationReceived(): void
{
}
#[LiveListener('notification:dismissed')]
public function onNotificationDismissed(): void
{
}
}π
Enforces that when a #[LiveProp] attribute specifies hydrateWith and dehydrateWith parameters:
- Both parameters must be specified together
- Both methods must exist in the component class and be declared as public
- The types must be compatible throughout the hydration/dehydration cycle:
- The property must have a type declaration
- The hydrate method must return the same type as the property
- The dehydrate method must accept the same type as the property as its first parameter
- The dehydrate method's return type must match the hydrate method's parameter type
This ensures data flows correctly between frontend and backend representations.
rules:
- Kocal\PHPStanSymfonyUX\Rules\LiveComponent\LivePropHydrationMethodsRule// src/Twig/Components/ProductList.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent]
final class ProductList
{
// Error: Missing dehydrateWith parameter
#[LiveProp(hydrateWith: 'hydrateFilters')]
public array $filters;
}// src/Twig/Components/ProductList.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent]
final class ProductList
{
#[LiveProp(hydrateWith: 'hydrateFilters', dehydrateWith: 'dehydrateFilters')]
public array $filters;
// Error: Methods are private/protected instead of public
private function hydrateFilters(array $data): array
{
return $data;
}
protected function dehydrateFilters(array $data): array
{
return $data;
}
}// src/Twig/Components/ShoppingCart.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
class Product
{
public function __construct(public string $name, public float $price) {}
}
#[AsLiveComponent]
final class ShoppingCart
{
#[LiveProp(hydrateWith: 'hydrateProduct', dehydrateWith: 'dehydrateProduct')]
public Product $product;
// Error: Return type doesn't match property type
public function hydrateProduct(array $data): array
{
return $data;
}
// Error: Parameter type doesn't match property type
public function dehydrateProduct(string $product): array
{
return [];
}
}β
// src/Twig/Components/ShoppingCart.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
class Product
{
public function __construct(public string $name, public float $price) {}
}
#[AsLiveComponent]
final class ShoppingCart
{
#[LiveProp(hydrateWith: 'hydrateProduct', dehydrateWith: 'dehydrateProduct')]
public Product $product;
public function hydrateProduct(array $data): Product
{
return new Product($data['name'], $data['price']);
}
public function dehydrateProduct(Product $product): array
{
return ['name' => $product->name, 'price' => $product->price];
}
}π
Note
All these rules also apply to LiveComponents (classes annotated with #[AsLiveComponent]).
Enforces that all Twig Component classes must be declared as final.
This prevents inheritance and promotes composition via traits, ensuring better code maintainability and avoiding tight coupling between components.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ClassMustBeFinalRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Alert
{
public string $message;
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
abstract class Alert
{
public string $message;
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $message;
}π
Forbid Twig Component class names from ending with "Component" suffix, as it creates redundancy since the class is already identified as a component through the #[AsTwigComponent] attribute.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ClassNameShouldNotEndWithComponentRule// src/Twig/Components/AlertComponent.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class AlertComponent
{
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
}π
Enforces that the #[AsTwigComponent] attribute has its exposePublicProps parameter explicitly set to false.
This prevents public properties from being automatically exposed to templates, promoting explicit control over what data is accessible in your Twig components.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ExposePublicPropsShouldBeFalseRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $message;
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent(exposePublicProps: true)]
final class Alert
{
public string $message;
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent(exposePublicProps: false)]
final class Alert
{
public string $message;
}π
Forbid the use of the $attributes property in Twig Components, which can lead to confusion when using {{ attributes }} (an instance of ComponentAttributes that is automatically injected) in Twig templates.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenAttributesPropertyRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public $attributes;
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent(attributesVar: 'customAttributes')]
final class Alert
{
public $customAttributes;
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public $customAttributes;
}π
Forbid the use of the $class property in Twig Components, as it is considered a bad practice to manipulate CSS classes directly in components.
Use {{ attributes }} or {{ attributes.defaults({ class: '...' }) }} in your Twig templates instead.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ForbiddenClassPropertyRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public $class;
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
}π
Enforces that all methods in Twig Components are either public or private, but not protected.
Since Twig Components must be final classes and inheritance is forbidden (see ForbiddenInheritanceRule), protected methods serve no purpose and should be avoided.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $message;
protected function formatMessage(): string
{
return strtoupper($this->message);
}
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $message;
public function formatMessage(): string
{
return strtoupper($this->message);
}
private function helperMethod(): void
{
// ...
}
}π
Enforces that methods with the #[PostMount] attribute have the correct signature: they must be public with an optional parameter of type array, and a return type of array, void, or array|void.
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PostMountMethodSignatureRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent]
final class Alert
{
#[PostMount]
protected function postMount(): void
{
}
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent]
final class Alert
{
#[PostMount]
public function postMount(array $data): string
{
return 'invalid';
}
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent]
final class Alert
{
#[PostMount]
public function postMount(string $data): void
{
}
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent]
final class Alert
{
#[PostMount]
public function postMount(): void
{
}
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent]
final class Alert
{
#[PostMount]
public function postMount(array $data): array
{
return $data;
}
}π
Enforces that methods with the #[PreMount] attribute have the correct signature: they must be public and have exactly one parameter of type array, with a return type of array, void, or array|void .
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PreMountMethodSignatureRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PreMount;
#[AsTwigComponent]
final class Alert
{
#[PreMount]
protected function preMount(array $data): array
{
return $data;
}
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PreMount;
#[AsTwigComponent]
final class Alert
{
#[PreMount]
public function preMount(string $data): array
{
return [];
}
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PreMount;
#[AsTwigComponent]
final class Alert
{
#[PreMount]
public function preMount(array $data): array
{
$data['timestamp'] = time();
return $data;
}
}π
Enforces that all public properties in Twig Components follow camelCase naming convention. This ensures consistency and better integration with Twig templates where properties are passed and accessed using camelCase.
rules:
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PublicPropertiesShouldBeCamelCaseRule// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $user_name;
public bool $is_active;
}// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $UserName;
}β
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $userName;
public bool $isActive;
}π