Skip to content

Commit bb09df6

Browse files
committed
Add PostMountMethodSignatureRule
1 parent 62a9145 commit bb09df6

34 files changed

+795
-187
lines changed

README.md

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ final class Alert
407407

408408
### PreMountMethodSignatureRule
409409

410-
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`.
410+
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` .
411411
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
412412

413413
```yaml
@@ -444,12 +444,17 @@ use Symfony\UX\TwigComponent\Attribute\PreMount;
444444
final class Alert
445445
{
446446
#[PreMount]
447-
public function preMount(array $data): void
447+
public function preMount(string $data): array
448448
{
449+
return [];
449450
}
450451
}
451452
```
452453

454+
:x:
455+
456+
<br>
457+
453458
```php
454459
// src/Twig/Components/Alert.php
455460
namespace App\Twig\Components;
@@ -461,9 +466,77 @@ use Symfony\UX\TwigComponent\Attribute\PreMount;
461466
final class Alert
462467
{
463468
#[PreMount]
464-
public function preMount(string $data): array
469+
public function preMount(array $data): array
470+
{
471+
$data['timestamp'] = time();
472+
473+
return $data;
474+
}
475+
}
476+
```
477+
478+
:+1:
479+
480+
<br>
481+
482+
### PostMountMethodSignatureRule
483+
484+
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`.
485+
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
486+
487+
```yaml
488+
rules:
489+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PostMountMethodSignatureRule
490+
```
491+
492+
```php
493+
// src/Twig/Components/Alert.php
494+
namespace App\Twig\Components;
495+
496+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
497+
use Symfony\UX\TwigComponent\Attribute\PostMount;
498+
499+
#[AsTwigComponent]
500+
final class Alert
501+
{
502+
#[PostMount]
503+
protected function postMount(): void
504+
{
505+
}
506+
}
507+
```
508+
509+
```php
510+
// src/Twig/Components/Alert.php
511+
namespace App\Twig\Components;
512+
513+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
514+
use Symfony\UX\TwigComponent\Attribute\PostMount;
515+
516+
#[AsTwigComponent]
517+
final class Alert
518+
{
519+
#[PostMount]
520+
public function postMount(array $data): string
521+
{
522+
return 'invalid';
523+
}
524+
}
525+
```
526+
527+
```php
528+
// src/Twig/Components/Alert.php
529+
namespace App\Twig\Components;
530+
531+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
532+
use Symfony\UX\TwigComponent\Attribute\PostMount;
533+
534+
#[AsTwigComponent]
535+
final class Alert
536+
{
537+
#[PostMount]
538+
public function postMount(string $data): void
465539
{
466-
return [];
467540
}
468541
}
469542
```
@@ -477,16 +550,31 @@ final class Alert
477550
namespace App\Twig\Components;
478551

479552
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
480-
use Symfony\UX\TwigComponent\Attribute\PreMount;
553+
use Symfony\UX\TwigComponent\Attribute\PostMount;
481554

482555
#[AsTwigComponent]
483556
final class Alert
484557
{
485-
#[PreMount]
486-
public function preMount(array $data): array
558+
#[PostMount]
559+
public function postMount(): void
487560
{
488-
$data['timestamp'] = time();
561+
}
562+
}
563+
```
489564

565+
```php
566+
// src/Twig/Components/Alert.php
567+
namespace App\Twig\Components;
568+
569+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
570+
use Symfony\UX\TwigComponent\Attribute\PostMount;
571+
572+
#[AsTwigComponent]
573+
final class Alert
574+
{
575+
#[PostMount]
576+
public function postMount(array $data): array
577+
{
490578
return $data;
491579
}
492580
}

ecs.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
->withPreparedSets(psr12: true, common: true)
1515
->withSkip([
1616
ProtectedToPrivateFixer::class => [
17-
__DIR__ . '/tests/Rules/TwigComponent/MethodsShouldBePublicOrPrivateRule/Fixture/*',
17+
__DIR__ . '/tests/Rules/**/Fixture/*',
1818
],
1919
])
2020
;

phpstan.dist.neon

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,4 @@ parameters:
1212
identifiers:
1313
- method.unused
1414
- missingType.iterableValue
15-
- missingType.return
1615
- property.unused
17-
- return.unusedType
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Rules\TwigComponent;
6+
7+
use Kocal\PHPStanSymfonyUX\NodeAnalyzer\AttributeFinder;
8+
use PhpParser\Node;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\ExtendedMethodReflection;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
15+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
16+
use Symfony\UX\TwigComponent\Attribute\PostMount;
17+
18+
/**
19+
* @implements Rule<Node\Stmt\Class_>
20+
*/
21+
final class PostMountMethodSignatureRule implements Rule
22+
{
23+
public function __construct(
24+
private ReflectionProvider $reflectionProvider,
25+
) {
26+
}
27+
28+
public function getNodeType(): string
29+
{
30+
return Node\Stmt\Class_::class;
31+
}
32+
33+
public function processNode(Node $node, Scope $scope): array
34+
{
35+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
36+
return [];
37+
}
38+
39+
if ($node->namespacedName === null) {
40+
return [];
41+
}
42+
43+
$reflClass = $this->reflectionProvider->getClass($node->namespacedName->toString());
44+
$errors = [];
45+
46+
foreach ($node->getMethods() as $method) {
47+
if (! AttributeFinder::findAttribute($method, PostMount::class)) {
48+
continue;
49+
}
50+
51+
$errors[] = $this->validatePostMountMethod(
52+
$method,
53+
$reflClass->getMethod($method->name->name, $scope),
54+
);
55+
}
56+
57+
return array_merge(...$errors);
58+
}
59+
60+
/**
61+
* @return list<\PHPStan\Rules\IdentifierRuleError>
62+
*/
63+
private function validatePostMountMethod(Node\Stmt\ClassMethod $method, ExtendedMethodReflection $reflMethod): array
64+
{
65+
$errors = [];
66+
67+
$methodName = $reflMethod->getName();
68+
$methodParams = $reflMethod->getOnlyVariant()->getParameters();
69+
$methodReturnType = $reflMethod->getOnlyVariant()->getReturnType();
70+
71+
// Check if method is public
72+
if (! $reflMethod->isPublic()) {
73+
$errors[] = RuleErrorBuilder::message(sprintf('Method "%s" with #[PostMount] attribute must be public.', $methodName))
74+
->identifier('symfonyUX.twigComponent.postMountPublic')
75+
->line($method->getLine())
76+
->tip('Change the method visibility to public.')
77+
->build();
78+
}
79+
80+
// Check parameter count and type (0 or 1 parameter allowed)
81+
if (count($methodParams) > 1) {
82+
$errors[] = RuleErrorBuilder::message(sprintf('Method "%s" with #[PostMount] attribute must have at most one parameter of type "array".', $methodName))
83+
->identifier('symfonyUX.twigComponent.postMountParameterCount')
84+
->line($method->getLine())
85+
->tip('The method should have zero or one parameter: "array $data" (optional).')
86+
->build();
87+
} elseif (count($methodParams) === 1) {
88+
// If there is a parameter, it must be of type array
89+
if (! $methodParams[0]->getType()->isArray()->yes()) {
90+
$errors[] = RuleErrorBuilder::message(sprintf('Method "%s" with #[PostMount] attribute must have a parameter of type "array".', $methodName))
91+
->identifier('symfonyUX.twigComponent.postMountParameterType')
92+
->line($method->getLine())
93+
->tip('Change the parameter type to "array".')
94+
->build();
95+
}
96+
}
97+
98+
// Check return type (must be array, void, or array|void)
99+
$isValidReturnType = $methodReturnType->isVoid()->yes()
100+
|| $methodReturnType->isArray()->yes()
101+
|| ($methodReturnType->isArray()->maybe() && $methodReturnType->isVoid()->maybe());
102+
103+
if (! $isValidReturnType) {
104+
$errors[] = RuleErrorBuilder::message(sprintf('Method "%s" with #[PostMount] attribute must have a return type of "array", "void", or "array|void".', $methodName))
105+
->identifier('symfonyUX.twigComponent.postMountReturnType')
106+
->line($method->getLine())
107+
->tip('Change the return type to ": array", ": void", or ": array|void".')
108+
->build();
109+
}
110+
111+
return $errors;
112+
}
113+
}

0 commit comments

Comments
 (0)