Skip to content

Commit 77f7112

Browse files
committed
Add PostMountMethodSignatureRule
1 parent 62a9145 commit 77f7112

34 files changed

+808
-200
lines changed

README.md

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -346,27 +346,30 @@ final class Alert
346346

347347
<br>
348348

349-
### PublicPropertiesShouldBeCamelCaseRule
349+
### PostMountMethodSignatureRule
350350

351-
Enforces that all public properties in Twig Components follow camelCase naming convention.
352-
This ensures consistency and better integration with Twig templates where properties are passed and accessed using camelCase.
351+
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`.
352+
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
353353

354354
```yaml
355355
rules:
356-
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PublicPropertiesShouldBeCamelCaseRule
356+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PostMountMethodSignatureRule
357357
```
358358
359359
```php
360360
// src/Twig/Components/Alert.php
361361
namespace App\Twig\Components;
362362

363363
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
364+
use Symfony\UX\TwigComponent\Attribute\PostMount;
364365

365366
#[AsTwigComponent]
366367
final class Alert
367368
{
368-
public string $user_name;
369-
public bool $is_active;
369+
#[PostMount]
370+
protected function postMount(): void
371+
{
372+
}
370373
}
371374
```
372375

@@ -375,11 +378,33 @@ final class Alert
375378
namespace App\Twig\Components;
376379

377380
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
381+
use Symfony\UX\TwigComponent\Attribute\PostMount;
378382

379383
#[AsTwigComponent]
380384
final class Alert
381385
{
382-
public string $UserName;
386+
#[PostMount]
387+
public function postMount(array $data): string
388+
{
389+
return 'invalid';
390+
}
391+
}
392+
```
393+
394+
```php
395+
// src/Twig/Components/Alert.php
396+
namespace App\Twig\Components;
397+
398+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
399+
use Symfony\UX\TwigComponent\Attribute\PostMount;
400+
401+
#[AsTwigComponent]
402+
final class Alert
403+
{
404+
#[PostMount]
405+
public function postMount(string $data): void
406+
{
407+
}
383408
}
384409
```
385410

@@ -392,12 +417,33 @@ final class Alert
392417
namespace App\Twig\Components;
393418

394419
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
420+
use Symfony\UX\TwigComponent\Attribute\PostMount;
395421

396422
#[AsTwigComponent]
397423
final class Alert
398424
{
399-
public string $userName;
400-
public bool $isActive;
425+
#[PostMount]
426+
public function postMount(): void
427+
{
428+
}
429+
}
430+
```
431+
432+
```php
433+
// src/Twig/Components/Alert.php
434+
namespace App\Twig\Components;
435+
436+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
437+
use Symfony\UX\TwigComponent\Attribute\PostMount;
438+
439+
#[AsTwigComponent]
440+
final class Alert
441+
{
442+
#[PostMount]
443+
public function postMount(array $data): array
444+
{
445+
return $data;
446+
}
401447
}
402448
```
403449

@@ -407,7 +453,7 @@ final class Alert
407453

408454
### PreMountMethodSignatureRule
409455

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`.
456+
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` .
411457
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
412458

413459
```yaml
@@ -444,12 +490,17 @@ use Symfony\UX\TwigComponent\Attribute\PreMount;
444490
final class Alert
445491
{
446492
#[PreMount]
447-
public function preMount(array $data): void
493+
public function preMount(string $data): array
448494
{
495+
return [];
449496
}
450497
}
451498
```
452499

500+
:x:
501+
502+
<br>
503+
453504
```php
454505
// src/Twig/Components/Alert.php
455506
namespace App\Twig\Components;
@@ -461,34 +512,71 @@ use Symfony\UX\TwigComponent\Attribute\PreMount;
461512
final class Alert
462513
{
463514
#[PreMount]
464-
public function preMount(string $data): array
515+
public function preMount(array $data): array
465516
{
466-
return [];
517+
$data['timestamp'] = time();
518+
519+
return $data;
467520
}
468521
}
469522
```
470523

471-
:x:
524+
:+1:
472525

473526
<br>
474527

528+
### PublicPropertiesShouldBeCamelCaseRule
529+
530+
Enforces that all public properties in Twig Components follow camelCase naming convention.
531+
This ensures consistency and better integration with Twig templates where properties are passed and accessed using camelCase.
532+
533+
```yaml
534+
rules:
535+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PublicPropertiesShouldBeCamelCaseRule
536+
```
537+
475538
```php
476539
// src/Twig/Components/Alert.php
477540
namespace App\Twig\Components;
478541

479542
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
480-
use Symfony\UX\TwigComponent\Attribute\PreMount;
481543

482544
#[AsTwigComponent]
483545
final class Alert
484546
{
485-
#[PreMount]
486-
public function preMount(array $data): array
487-
{
488-
$data['timestamp'] = time();
547+
public string $user_name;
548+
public bool $is_active;
549+
}
550+
```
489551

490-
return $data;
491-
}
552+
```php
553+
// src/Twig/Components/Alert.php
554+
namespace App\Twig\Components;
555+
556+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
557+
558+
#[AsTwigComponent]
559+
final class Alert
560+
{
561+
public string $UserName;
562+
}
563+
```
564+
565+
:x:
566+
567+
<br>
568+
569+
```php
570+
// src/Twig/Components/Alert.php
571+
namespace App\Twig\Components;
572+
573+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
574+
575+
#[AsTwigComponent]
576+
final class Alert
577+
{
578+
public string $userName;
579+
public bool $isActive;
492580
}
493581
```
494582

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)