Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/FileExtractor/PHPFileExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ final class PHPFileExtractor implements FileExtractor
public function getSourceLocations(SplFileInfo $file, SourceCollection $collection): void
{
$path = $file->getRelativePath();
/** @phpstan-ignore-next-line */
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString('8.1'));
$traverser = new NodeTraverser();
$traverser->addVisitor(new NodeVisitor\NameResolver());
foreach ($this->visitors as $v) {
$v->init($collection, $file);
$traverser->addVisitor($v);
Expand Down
3 changes: 2 additions & 1 deletion src/Visitor/Php/Knp/Menu/AbstractKnpMenuVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ protected function isKnpMenuBuildingMethod(Node $node): bool
if (!$returnType instanceof Node\Name) {
$this->isKnpMenuBuildingMethod = false;
} else {
$this->isKnpMenuBuildingMethod = 'ItemInterface' === $returnType->toString();
$this->isKnpMenuBuildingMethod = 'ItemInterface' === $returnType->toString()
|| str_ends_with($returnType->toString(), '\\ItemInterface');
}
}

Expand Down
111 changes: 103 additions & 8 deletions src/Visitor/Php/Symfony/FormTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,117 @@
namespace Translation\Extractor\Visitor\Php\Symfony;

use PhpParser\Node;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use PhpParser\PhpVersion;

trait FormTrait
{
private bool $isFormType = false;
private array $classMap = [];
private string $symfonyInterface = 'FormTypeInterface';

/**
* Check if this node is a form type.
*/
private function isFormType(Node $node): bool
protected function isFormType(Node $node): bool
{
// only Traverse *Type
if ($node instanceof Stmt\Class_) {
$this->isFormType = 'Type' === substr($node->name, -4);
if (!$node instanceof Class_) {
return $this->isFormType;
}

$allInterfaces = $this->getAllInterfacesFromNode($node);

foreach ($allInterfaces as $interface) {
if ($interface === $this->symfonyInterface
|| str_ends_with($interface, '\\'.$this->symfonyInterface)) {
$this->isFormType = true;
}
}

return $this->isFormType;
}

protected function getAllInterfacesFromNode(Class_ $node): array
{
$interfaces = [];

foreach ($node->implements as $interface) {
$interfaces[] = $interface->toString();
}

if ($node->extends) {
$parentFqcn = $node->extends->toString();

$parentInterfaces = $this->loadParentInterfaces($parentFqcn);
$interfaces = array_merge($interfaces, $parentInterfaces);
}

return array_unique($interfaces);
}

protected function loadParentInterfaces(string $parentFqcn): array
{
$interfaces = [];

static $loading = [];
if (isset($loading[$parentFqcn])) {
return [];
}
$loading[$parentFqcn] = true;

try {
$filePath = $this->findClassFile($parentFqcn);

if (!$filePath || !file_exists($filePath)) {
unset($loading[$parentFqcn]);

return [];
}

/** @phpstan-ignore-next-line */
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString('8.1'));
$code = file_get_contents($filePath);
$stmts = $parser->parse($code);

$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$stmts = $traverser->traverse($stmts);

foreach ($stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Namespace_) {
foreach ($stmt->stmts as $subStmt) {
if ($subStmt instanceof Class_) {
$interfaces = array_merge(
$interfaces,
$this->getAllInterfacesFromNode($subStmt)
);
}
}
} elseif ($stmt instanceof Class_) {
$interfaces = array_merge(
$interfaces,
$this->getAllInterfacesFromNode($stmt)
);
}
}
} catch (\Exception $e) {
}

unset($loading[$parentFqcn]);

return $interfaces;
}

private function findClassFile(string $fqcn): ?string
{
$autoloadFile = __DIR__.'/../../../../vendor/autoload.php';

if (!file_exists($autoloadFile)) {
return null;
}

$loader = require $autoloadFile;

return $loader->findFile($fqcn) ?: null;
}
}
44 changes: 44 additions & 0 deletions tests/Functional/Visitor/Php/Symfony/IsFormTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Translation\Extractor\Tests\Functional\Visitor\Php\Symfony;

use Translation\Extractor\Tests\Functional\Visitor\Php\BasePHPVisitorTest;
use Translation\Extractor\Tests\Resources;
use Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelExplicit;

class IsFormTypeTest extends BasePHPVisitorTest
{
public function testParentFormTypeExtractsTranslations(): void
{
$collection = $this->getSourceLocations(
new FormTypeLabelExplicit(),
Resources\Php\Symfony\ParentType::class
);

$this->assertCount(1, $collection);
$source = $collection->first();
$this->assertEquals('parent.field.label', $source->getMessage());
}

public function testInheritedFormTypeIsProperlyDetected(): void
{
$collection = $this->getSourceLocations(
new FormTypeLabelExplicit(),
Resources\Php\Symfony\IsFormType::class
);

$this->assertCount(1, $collection);
$source = $collection->first();
$this->assertEquals('child.field.label', $source->getMessage());
}

public function testNonFormTypeDoesNotExtractTranslations(): void
{
$collection = $this->getSourceLocations(
new FormTypeLabelExplicit(),
Resources\Php\Symfony\NotAFormType::class
);

$this->assertCount(0, $collection);
}
}
2 changes: 1 addition & 1 deletion tests/Resources/Github/Issue_109.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Translation\Extractor\Tests\Resources\Github;
use Translation\Extractor\Annotation\Ignore;

class MustNotBeIgnoredType
class MustNotBeIgnoredType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Github/Issue_111.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Translation\Extractor\Annotation\Ignore;

class Issue111Type
class Issue111Type implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Github/Issue_62.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use Symfony\Component\Form\AbstractType;
use Translation\Extractor\Annotation\Ignore;

class EmptyValueType extends AbstractType
class EmptyValueType implements FormTypeInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, extends AbstractType should also work, right? I suppose we should revert it.

I mean, we should check both: extends AbstractType and implements FormTypeInterface

I see you already check for implements FormTypeInterface in other places, so I would prefer to revert back to extends AbstractType in those spots where you changed to interface to make sure it's still works too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i extends AbstractType i need to require dev symfony/form or create a AbstractType class that implements FormTypeInterface in test/Resources/Php/Symfony.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I see.. Well, then I'm fine with using the interface. Though it would be cool to have a test that will cover the case with an extended base class like AbstractType to make sure it finds that interface implemented even in the parent base class we extend. Maybe creating a custom AbstractType that implements FormTypeInterface just for test purposes would make sense 👍🏼

Copy link
Contributor Author

@gchehami gchehami Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a test, where a formType extends a parentType that implements FormTypeInterface, but if you prefer to call it AbstractType i can change the name of the parentType class

The test is : IsFormTypeTest

I made it because i needed to validate my recursive verification

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, I missed that... That's good to have this test to check the recursive feature, good job 👍🏼

{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Github/Issue_78.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;

class EmptyValueType extends AbstractType
class EmptyValueType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Github/Issue_80b.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use Symfony\Component\Form\AbstractType;

class PlaceholderAsBooleanType extends AbstractType
class PlaceholderAsBooleanType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Github/issue_82.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Translation\Extractor\Tests\Resources\Github;

class GlobalTranslationDomainType
class GlobalTranslationDomainType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Github/issue_96.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Translation\Extractor\Tests\Resources\Github;

class GlobalTranslationDomainWithPlaceholderType
class GlobalTranslationDomainWithPlaceholderType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/ChainedChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Translation\Extractor\Tests\Resources\Php\Symfony;

class ChainedChoiceType
class ChainedChoiceType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/DescriptionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Symfony\Component\Form\FormBuilderInterface;
use Translation\Extractor\Annotation\Desc;

class DescriptionType extends AbstractType
class DescriptionType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/EmptyValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class EmptyValueType extends AbstractType
class EmptyValueType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/ExplicitLabelFalseType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use Symfony\Component\Form\AbstractType;
use Translation\Extractor\Annotation\Ignore;

class ExplicitLabelFalseType extends AbstractType
class ExplicitLabelFalseType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/ExplicitLabelType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Translation\Extractor\Annotation\Ignore;

class ExplicitLabelType
class ExplicitLabelType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
4 changes: 2 additions & 2 deletions tests/Resources/Php/Symfony/FormDomainChoiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Translation\Extractor\Tests\Resources\Php\Symfony;

class FormDomainChoiceType
class FormDomainChoiceType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down Expand Up @@ -31,4 +31,4 @@ public function buildForm(FormBuilderInterface $builder, array $options)
'choice_translation_domain' => false,
]);
}
}
}
4 changes: 2 additions & 2 deletions tests/Resources/Php/Symfony/FormDomainType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Translation\Extractor\Tests\Resources\Php\Symfony;

class FormDomainType
class FormDomainType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand All @@ -29,4 +29,4 @@ public function buildForm(FormBuilderInterface $builder, array $options)
'translation_domain' => false,
]);
}
}
}
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/FormInvalidMessageType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class FormInvalidMessageType extends AbstractType
class FormInvalidMessageType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/HelpFormErrorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Symfony\Component\Form\FormBuilderInterface;
use Translation\Extractor\Annotation\Ignore;

class PlaceholderFormErrorType extends AbstractType
class PlaceholderFormErrorType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/HelpFormType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class HelpFormType extends AbstractType
class HelpFormType implements FormTypeInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Resources/Php/Symfony/ImplicitLabelType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Translation\Extractor\Tests\Resources\Php\Symfony;

class ImplicitLabelType
class ImplicitLabelType implements FormTypeInterface
{
const ISSUE_131 = 'issue_131';

Expand Down
17 changes: 17 additions & 0 deletions tests/Resources/Php/Symfony/IsFormType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Translation\Extractor\Tests\Resources\Php\Symfony;

use Symfony\Component\Form\FormBuilderInterface;

class IsFormType extends ParentType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);

$builder->add('child_field', null, [
'label' => 'child.field.label'
]);
}
}
Loading
Loading