Skip to content

Commit a8dacdb

Browse files
author
Guillaume Chehami
committed
ref#184 checking if class implements Symfony\Component\Form\FormTypeInterface instead of checking the classname to determinate if the class is a form
fix phpstan Call to an undefined method PhpParser\ParserFactory::createForVersion()
1 parent 5924447 commit a8dacdb

34 files changed

Lines changed: 223 additions & 38 deletions

src/FileExtractor/PHPFileExtractor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public function getSourceLocations(SplFileInfo $file, SourceCollection $collecti
3535
$path = $file->getRelativePath();
3636
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString('8.1'));
3737
$traverser = new NodeTraverser();
38+
$traverser->addVisitor(new NodeVisitor\NameResolver());
3839
foreach ($this->visitors as $v) {
3940
$v->init($collection, $file);
4041
$traverser->addVisitor($v);

src/Visitor/Php/Knp/Menu/AbstractKnpMenuVisitor.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ protected function isKnpMenuBuildingMethod(Node $node): bool
9696
if (!$returnType instanceof Node\Name) {
9797
$this->isKnpMenuBuildingMethod = false;
9898
} else {
99-
$this->isKnpMenuBuildingMethod = 'ItemInterface' === $returnType->toString();
99+
$this->isKnpMenuBuildingMethod = 'ItemInterface' === $returnType->toString()
100+
|| str_ends_with($returnType->toString(), '\\ItemInterface');
100101
}
101102
}
102103

src/Visitor/Php/Symfony/FormTrait.php

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,116 @@
1212
namespace Translation\Extractor\Visitor\Php\Symfony;
1313

1414
use PhpParser\Node;
15-
use PhpParser\Node\Stmt;
15+
use PhpParser\Node\Stmt\Class_;
16+
use PhpParser\NodeTraverser;
17+
use PhpParser\NodeVisitor\NameResolver;
18+
use PhpParser\ParserFactory;
19+
use PhpParser\PhpVersion;
1620

1721
trait FormTrait
1822
{
1923
private bool $isFormType = false;
24+
private array $classMap = [];
25+
private string $symfonyInterface = 'FormTypeInterface';
2026

21-
/**
22-
* Check if this node is a form type.
23-
*/
24-
private function isFormType(Node $node): bool
27+
protected function isFormType(Node $node): bool
2528
{
26-
// only Traverse *Type
27-
if ($node instanceof Stmt\Class_) {
28-
$this->isFormType = 'Type' === substr($node->name, -4);
29+
if (!$node instanceof Class_) {
30+
return $this->isFormType;
31+
}
32+
33+
$allInterfaces = $this->getAllInterfacesFromNode($node);
34+
35+
foreach ($allInterfaces as $interface) {
36+
if ($interface === $this->symfonyInterface
37+
|| str_ends_with($interface, '\\'.$this->symfonyInterface)) {
38+
$this->isFormType = true;
39+
}
2940
}
3041

3142
return $this->isFormType;
3243
}
44+
45+
protected function getAllInterfacesFromNode(Class_ $node): array
46+
{
47+
$interfaces = [];
48+
49+
foreach ($node->implements as $interface) {
50+
$interfaces[] = $interface->toString();
51+
}
52+
53+
if ($node->extends) {
54+
$parentFqcn = $node->extends->toString();
55+
56+
$parentInterfaces = $this->loadParentInterfaces($parentFqcn);
57+
$interfaces = array_merge($interfaces, $parentInterfaces);
58+
}
59+
60+
return array_unique($interfaces);
61+
}
62+
63+
protected function loadParentInterfaces(string $parentFqcn): array
64+
{
65+
$interfaces = [];
66+
67+
static $loading = [];
68+
if (isset($loading[$parentFqcn])) {
69+
return [];
70+
}
71+
$loading[$parentFqcn] = true;
72+
73+
try {
74+
$filePath = $this->findClassFile($parentFqcn);
75+
76+
if (!$filePath || !file_exists($filePath)) {
77+
unset($loading[$parentFqcn]);
78+
79+
return [];
80+
}
81+
82+
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString('8.1'));
83+
$code = file_get_contents($filePath);
84+
$stmts = $parser->parse($code);
85+
86+
$traverser = new NodeTraverser();
87+
$traverser->addVisitor(new NameResolver());
88+
$stmts = $traverser->traverse($stmts);
89+
90+
foreach ($stmts as $stmt) {
91+
if ($stmt instanceof Node\Stmt\Namespace_) {
92+
foreach ($stmt->stmts as $subStmt) {
93+
if ($subStmt instanceof Class_) {
94+
$interfaces = array_merge(
95+
$interfaces,
96+
$this->getAllInterfacesFromNode($subStmt)
97+
);
98+
}
99+
}
100+
} elseif ($stmt instanceof Class_) {
101+
$interfaces = array_merge(
102+
$interfaces,
103+
$this->getAllInterfacesFromNode($stmt)
104+
);
105+
}
106+
}
107+
} catch (\Exception $e) {
108+
}
109+
110+
unset($loading[$parentFqcn]);
111+
112+
return $interfaces;
113+
}
114+
115+
private function findClassFile(string $fqcn): ?string
116+
{
117+
$autoloadFile = __DIR__.'/../../../../vendor/autoload.php';
118+
119+
if (!file_exists($autoloadFile)) {
120+
return null;
121+
}
122+
123+
$loader = require $autoloadFile;
124+
125+
return $loader->findFile($fqcn) ?: null;
126+
}
33127
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Translation\Extractor\Tests\Functional\Visitor\Php\Symfony;
4+
5+
use Translation\Extractor\Tests\Functional\Visitor\Php\BasePHPVisitorTest;
6+
use Translation\Extractor\Tests\Resources;
7+
use Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelExplicit;
8+
9+
class IsFormTypeTest extends BasePHPVisitorTest
10+
{
11+
public function testParentFormTypeExtractsTranslations(): void
12+
{
13+
$collection = $this->getSourceLocations(
14+
new FormTypeLabelExplicit(),
15+
Resources\Php\Symfony\ParentType::class
16+
);
17+
18+
$this->assertCount(1, $collection);
19+
$source = $collection->first();
20+
$this->assertEquals('parent.field.label', $source->getMessage());
21+
}
22+
23+
public function testInheritedFormTypeIsProperlyDetected(): void
24+
{
25+
$collection = $this->getSourceLocations(
26+
new FormTypeLabelExplicit(),
27+
Resources\Php\Symfony\IsFormType::class
28+
);
29+
30+
$this->assertCount(1, $collection);
31+
$source = $collection->first();
32+
$this->assertEquals('child.field.label', $source->getMessage());
33+
}
34+
35+
public function testNonFormTypeDoesNotExtractTranslations(): void
36+
{
37+
$collection = $this->getSourceLocations(
38+
new FormTypeLabelExplicit(),
39+
Resources\Php\Symfony\NotAFormType::class
40+
);
41+
42+
$this->assertCount(0, $collection);
43+
}
44+
}

tests/Resources/Github/Issue_109.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Translation\Extractor\Tests\Resources\Github;
44
use Translation\Extractor\Annotation\Ignore;
55

6-
class MustNotBeIgnoredType
6+
class MustNotBeIgnoredType implements FormTypeInterface
77
{
88
public function buildForm(FormBuilderInterface $builder, array $options)
99
{

tests/Resources/Github/Issue_111.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use Translation\Extractor\Annotation\Ignore;
66

7-
class Issue111Type
7+
class Issue111Type implements FormTypeInterface
88
{
99
public function buildForm(FormBuilderInterface $builder, array $options)
1010
{

tests/Resources/Github/Issue_62.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use Symfony\Component\Form\AbstractType;
44
use Translation\Extractor\Annotation\Ignore;
55

6-
class EmptyValueType extends AbstractType
6+
class EmptyValueType implements FormTypeInterface
77
{
88
public function buildForm(FormBuilderInterface $builder, array $options)
99
{

tests/Resources/Github/Issue_78.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use Symfony\Component\Form\AbstractType;
44
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
55

6-
class EmptyValueType extends AbstractType
6+
class EmptyValueType implements FormTypeInterface
77
{
88
public function buildForm(FormBuilderInterface $builder, array $options)
99
{

tests/Resources/Github/Issue_80b.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use Symfony\Component\Form\AbstractType;
44

5-
class PlaceholderAsBooleanType extends AbstractType
5+
class PlaceholderAsBooleanType implements FormTypeInterface
66
{
77
public function buildForm(FormBuilderInterface $builder, array $options)
88
{

tests/Resources/Github/issue_82.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Translation\Extractor\Tests\Resources\Github;
44

5-
class GlobalTranslationDomainType
5+
class GlobalTranslationDomainType implements FormTypeInterface
66
{
77
public function buildForm(FormBuilderInterface $builder, array $options)
88
{

0 commit comments

Comments
 (0)