diff --git a/config/set/knplabs/doctrine-gedmo-to-knplabs.yaml b/config/set/knplabs/doctrine-gedmo-to-knplabs.yaml index c443bbee642..e9977512b35 100644 --- a/config/set/knplabs/doctrine-gedmo-to-knplabs.yaml +++ b/config/set/knplabs/doctrine-gedmo-to-knplabs.yaml @@ -3,3 +3,4 @@ services: Rector\DoctrineGedmoToKnplabs\Rector\Class_\TimestampableBehaviorRector: null Rector\DoctrineGedmoToKnplabs\Rector\Class_\SluggableBehaviorRector: null Rector\DoctrineGedmoToKnplabs\Rector\Class_\TreeBehaviorRector: ~ + Rector\DoctrineGedmoToKnplabs\Rector\Class_\TranslationBehaviorRector: ~ diff --git a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php index 43d3c29a371..cb55dd0097a 100644 --- a/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php +++ b/packages/BetterPhpDocParser/src/PhpDocInfo/PhpDocInfo.php @@ -220,6 +220,23 @@ final class PhpDocInfo return null; } + public function removeByType(string $type): void + { + $this->ensureTypeIsTagValueNode($type, __METHOD__); + + foreach ($this->phpDocNode->children as $key => $phpDocChildNode) { + if (! $phpDocChildNode instanceof PhpDocTagNode) { + continue; + } + + if (! is_a($phpDocChildNode->value, $type, true)) { + continue; + } + + unset($this->phpDocNode->children[$key]); + } + } + private function getParamTagValueByName(string $name): ?AttributeAwareParamTagValueNode { $phpDocNode = $this->getPhpDocNode(); diff --git a/packages/BetterPhpDocParser/src/PhpDocNode/Doctrine/Class_/EntityTagValueNode.php b/packages/BetterPhpDocParser/src/PhpDocNode/Doctrine/Class_/EntityTagValueNode.php index a71cc3e0a27..6388e1cc106 100644 --- a/packages/BetterPhpDocParser/src/PhpDocNode/Doctrine/Class_/EntityTagValueNode.php +++ b/packages/BetterPhpDocParser/src/PhpDocNode/Doctrine/Class_/EntityTagValueNode.php @@ -19,12 +19,15 @@ final class EntityTagValueNode extends AbstractDoctrineTagValueNode private $repositoryClass; /** - * @var bool + * @var bool|null */ - private $readOnly = false; + private $readOnly; - public function __construct(?string $repositoryClass, bool $readOnly, ?string $originalContent) - { + public function __construct( + ?string $repositoryClass = null, + ?bool $readOnly = null, + ?string $originalContent = null + ) { $this->repositoryClass = $repositoryClass; $this->readOnly = $readOnly; @@ -39,7 +42,9 @@ final class EntityTagValueNode extends AbstractDoctrineTagValueNode $contentItems['repositoryClass'] = sprintf('repositoryClass="%s"', $this->repositoryClass); } - $contentItems['readOnly'] = sprintf('readOnly=%s', $this->readOnly ? 'true' : 'false'); + if ($this->readOnly !== null) { + $contentItems['readOnly'] = sprintf('readOnly=%s', $this->readOnly ? 'true' : 'false'); + } return $this->printContentItems($contentItems); } diff --git a/packages/BetterPhpDocParser/src/PhpDocNode/Gedmo/LocaleTagValueNode.php b/packages/BetterPhpDocParser/src/PhpDocNode/Gedmo/LocaleTagValueNode.php new file mode 100644 index 00000000000..b6ff70c17f8 --- /dev/null +++ b/packages/BetterPhpDocParser/src/PhpDocNode/Gedmo/LocaleTagValueNode.php @@ -0,0 +1,21 @@ +createFromNode($node); + } + + protected function getTagValueNodeClass(): string + { + return LocaleTagValueNode::class; + } +} diff --git a/packages/BetterPhpDocParser/src/PhpDocNodeFactory/Gedmo/TranslatablePhpDocNodeFactory.php b/packages/BetterPhpDocParser/src/PhpDocNodeFactory/Gedmo/TranslatablePhpDocNodeFactory.php new file mode 100644 index 00000000000..4778b3b0319 --- /dev/null +++ b/packages/BetterPhpDocParser/src/PhpDocNodeFactory/Gedmo/TranslatablePhpDocNodeFactory.php @@ -0,0 +1,33 @@ +createFromNode($node); + } + + protected function getTagValueNodeClass(): string + { + return TranslatableTagValueNode::class; + } +} diff --git a/packages/DeadCode/src/Rector/ClassMethod/RemoveUnusedParameterRector.php b/packages/DeadCode/src/Rector/ClassMethod/RemoveUnusedParameterRector.php index 7eefd750c10..1146bb18b95 100644 --- a/packages/DeadCode/src/Rector/ClassMethod/RemoveUnusedParameterRector.php +++ b/packages/DeadCode/src/Rector/ClassMethod/RemoveUnusedParameterRector.php @@ -12,7 +12,6 @@ use Rector\NodeContainer\ParsedNodesByType; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PhpParser\Node\Manipulator\ClassManipulator; use Rector\PhpParser\Node\Manipulator\ClassMethodManipulator; -use Rector\PhpParser\Printer\BetterStandardPrinter; use Rector\Rector\AbstractRector; use Rector\RectorDefinition\CodeSample; use Rector\RectorDefinition\RectorDefinition; @@ -58,21 +57,14 @@ final class RemoveUnusedParameterRector extends AbstractRector '__wakeup', ]; - /** - * @var BetterStandardPrinter - */ - private $betterStandardPrinter; - public function __construct( ClassManipulator $classManipulator, ClassMethodManipulator $classMethodManipulator, - ParsedNodesByType $parsedNodesByType, - BetterStandardPrinter $betterStandardPrinter + ParsedNodesByType $parsedNodesByType ) { $this->classManipulator = $classManipulator; $this->classMethodManipulator = $classMethodManipulator; $this->parsedNodesByType = $parsedNodesByType; - $this->betterStandardPrinter = $betterStandardPrinter; } public function getDefinition(): RectorDefinition @@ -187,7 +179,7 @@ PHP $parameters1, $parameters2, function (Param $a, Param $b): int { - return $this->betterStandardPrinter->areNodesEqual($a, $b) ? 0 : 1; + return $this->areNodesEqual($a, $b) ? 0 : 1; } ); } diff --git a/packages/Doctrine/src/PhpDocParser/Ast/PhpDoc/PhpDocTagNodeFactory.php b/packages/Doctrine/src/PhpDocParser/Ast/PhpDoc/PhpDocTagNodeFactory.php index f3bf6655f78..0f6f38c87d1 100644 --- a/packages/Doctrine/src/PhpDocParser/Ast/PhpDoc/PhpDocTagNodeFactory.php +++ b/packages/Doctrine/src/PhpDocParser/Ast/PhpDoc/PhpDocTagNodeFactory.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use Ramsey\Uuid\UuidInterface; use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\SpacelessPhpDocTagNode; +use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Class_\EntityTagValueNode; use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Property_\ColumnTagValueNode; use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Property_\GeneratedValueTagValueNode; use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Property_\IdTagValueNode; @@ -49,6 +50,11 @@ final class PhpDocTagNodeFactory return new SpacelessPhpDocTagNode(IdTagValueNode::SHORT_NAME, new IdTagValueNode()); } + public function createEntityTag(): PhpDocTagNode + { + return new SpacelessPhpDocTagNode(EntityTagValueNode::SHORT_NAME, new EntityTagValueNode()); + } + public function createIdColumnTag(): PhpDocTagNode { $columnTagValueNode = new ColumnTagValueNode(null, 'integer', null, null, null, null, null); diff --git a/packages/DoctrineGedmoToKnplabs/src/Rector/Class_/TranslationBehaviorRector.php b/packages/DoctrineGedmoToKnplabs/src/Rector/Class_/TranslationBehaviorRector.php new file mode 100644 index 00000000000..99849390202 --- /dev/null +++ b/packages/DoctrineGedmoToKnplabs/src/Rector/Class_/TranslationBehaviorRector.php @@ -0,0 +1,273 @@ +classManipulator = $classManipulator; + $this->phpDocTagNodeFactory = $phpDocTagNodeFactory; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Change Translation from gedmo/doctrine-extensions to knplabs/doctrine-behaviors', [ + new CodeSample( + <<<'PHP' +use Gedmo\Mapping\Annotation as Gedmo; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translatable\Translatable; + +/** + * @ORM\Table + */ +class Article implements Translatable +{ + /** + * @Gedmo\Translatable + * @ORM\Column(length=128) + */ + private $title; + + /** + * @Gedmo\Translatable + * @ORM\Column(type="text") + */ + private $content; + + /** + * @Gedmo\Locale + * Used locale to override Translation listener`s locale + * this is not a mapped field of entity metadata, just a simple property + * and it is not necessary because globally locale can be set in listener + */ + private $locale; + + public function setTitle($title) + { + $this->title = $title; + } + + public function getTitle() + { + return $this->title; + } + + public function setContent($content) + { + $this->content = $content; + } + + public function getContent() + { + return $this->content; + } + + public function setTranslatableLocale($locale) + { + $this->locale = $locale; + } +} +PHP +, + <<<'PHP' +use Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait; +use Knp\DoctrineBehaviors\Contract\Entity\TranslatableInterface; + +class SomeClass implements TranslatableInterface +{ + use TranslatableTrait; +} + + +use Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface; +use Knp\DoctrineBehaviors\Model\Translatable\TranslationTrait; + +class SomeClassTranslation implements TranslationInterface +{ + use TranslationTrait; + + /** + * @ORM\Column(length=128) + */ + private $title; + + /** + * @ORM\Column(type="text") + */ + private $content; +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->classManipulator->hasInterface($node, 'Gedmo\Translatable\Translatable')) { + return null; + } + + $this->classManipulator->removeInterface($node, 'Gedmo\Translatable\Translatable'); + + // 1. replace trait + $this->classManipulator->addAsFirstTrait($node, 'Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait'); + + // 2. add interface + $node->implements[] = new FullyQualified('Knp\DoctrineBehaviors\Contract\Entity\TranslatableInterface'); + + $removedPropertyNameToPhpDocInfo = $this->collectAndRemoveTranslatableProperties($node); + $removePropertyNames = array_keys($removedPropertyNameToPhpDocInfo); + + // @todo add them as a @method annotation, so the autocomplete still works? + $this->removeSetAndGetMethods($node, $removePropertyNames); + + $this->dumpEntityTranslation($node, $removedPropertyNameToPhpDocInfo); + + return $node; + } + + /** + * @param string[] $removedPropertyNames + */ + private function removeSetAndGetMethods(Class_ $class, array $removedPropertyNames): void + { + foreach ($removedPropertyNames as $removedPropertyName) { + foreach ($class->getMethods() as $method) { + if ($this->isName($method, 'set' . ucfirst($removedPropertyName))) { + $this->removeNode($method); + } + + if ($this->isName($method, 'get' . ucfirst($removedPropertyName))) { + $this->removeNode($method); + } + + if ($this->isName($method, 'setTranslatableLocale')) { + $this->removeNode($method); + } + } + } + } + + /** + * @return PhpDocInfo[] + */ + private function collectAndRemoveTranslatableProperties(Class_ $class): array + { + $removedPropertyNameToPhpDocInfo = []; + + foreach ($class->getProperties() as $property) { + $propertyPhpDocInfo = $this->getPhpDocInfo($property); + if ($propertyPhpDocInfo === null) { + continue; + } + + if ($propertyPhpDocInfo->getByType(LocaleTagValueNode::class)) { + $this->removeNode($property); + continue; + } + + if (! $propertyPhpDocInfo->getByType(TranslatableTagValueNode::class)) { + continue; + } + + $propertyPhpDocInfo->removeByType(TranslatableTagValueNode::class); + + $propertyName = $this->getName($property); + $removedPropertyNameToPhpDocInfo[$propertyName] = $propertyPhpDocInfo; + + $this->removeNode($property); + } + + return $removedPropertyNameToPhpDocInfo; + } + + /** + * @param PhpDocInfo[] $translatedPropertyToPhpDocInfos + */ + private function dumpEntityTranslation(Class_ $class, array $translatedPropertyToPhpDocInfos): void + { + /** @var SmartFileInfo|null $fileInfo */ + $fileInfo = $class->getAttribute(AttributeKey::FILE_INFO); + if ($fileInfo === null) { + throw new ShouldNotHappenException(); + } + + $classShortName = (string) $class->name . 'Translation'; + $filePath = dirname($fileInfo->getRealPath()) . DIRECTORY_SEPARATOR . $classShortName . '.php'; + + $namespace = $class->getAttribute(AttributeKey::PARENT_NODE); + if (! $namespace instanceof Namespace_) { + throw new ShouldNotHappenException(); + } + + $namespace = new Namespace_($namespace->name); + + $class = new Class_($classShortName); + $class->implements[] = new FullyQualified('Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface'); + $this->classManipulator->addAsFirstTrait($class, 'Knp\DoctrineBehaviors\Model\Translatable\TranslationTrait'); + + $this->docBlockManipulator->addTag($class, $this->phpDocTagNodeFactory->createEntityTag()); + + foreach ($translatedPropertyToPhpDocInfos as $translatedPropertyName => $translatedPhpDocInfo) { + $property = $this->nodeFactory->createPrivateProperty($translatedPropertyName); + $this->docBlockManipulator->updateNodeWithPhpDocInfo($property, $translatedPhpDocInfo); + + $class->stmts[] = $property; + } + + $namespace->stmts[] = $class; + + // @todo make temporary + $content = 'print($namespace) . PHP_EOL; + + FileSystem::write($filePath, $content); + } +} diff --git a/packages/DoctrineGedmoToKnplabs/tests/Rector/Class_/TranslationBehaviorRector/Fixture/fixture.php.inc b/packages/DoctrineGedmoToKnplabs/tests/Rector/Class_/TranslationBehaviorRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..e73e837f37e --- /dev/null +++ b/packages/DoctrineGedmoToKnplabs/tests/Rector/Class_/TranslationBehaviorRector/Fixture/fixture.php.inc @@ -0,0 +1,78 @@ +title = $title; + } + + public function getTitle() + { + return $this->title; + } + + public function setContent($content) + { + $this->content = $content; + } + + public function getContent() + { + return $this->content; + } + + public function setTranslatableLocale($locale) + { + $this->locale = $locale; + } +} + +?> +----- + diff --git a/packages/DoctrineGedmoToKnplabs/tests/Rector/Class_/TranslationBehaviorRector/Source/ExpectedSomeClassTranslation.php b/packages/DoctrineGedmoToKnplabs/tests/Rector/Class_/TranslationBehaviorRector/Source/ExpectedSomeClassTranslation.php new file mode 100644 index 00000000000..3a9e4eea2d9 --- /dev/null +++ b/packages/DoctrineGedmoToKnplabs/tests/Rector/Class_/TranslationBehaviorRector/Source/ExpectedSomeClassTranslation.php @@ -0,0 +1,18 @@ +doTestFile(__DIR__ . '/Fixture/fixture.php.inc'); + + $generatedFile = sys_get_temp_dir() . '/rector_temp_tests/SomeClassTranslation.php'; + $this->assertFileExists($generatedFile); + + $this->assertFileEquals(__DIR__ . '/Source/ExpectedSomeClassTranslation.php', $generatedFile); + } + + protected function getRectorClass(): string + { + return TranslationBehaviorRector::class; + } +} diff --git a/packages/NetteToSymfony/src/Rector/Assign/FormControlToControllerAndFormTypeRector.php b/packages/NetteToSymfony/src/Rector/Assign/FormControlToControllerAndFormTypeRector.php index f668cf07532..25f85347de7 100644 --- a/packages/NetteToSymfony/src/Rector/Assign/FormControlToControllerAndFormTypeRector.php +++ b/packages/NetteToSymfony/src/Rector/Assign/FormControlToControllerAndFormTypeRector.php @@ -28,7 +28,6 @@ use PhpParser\Node\Stmt\Namespace_; use Rector\CodingStyle\Naming\ClassNaming; use Rector\NetteToSymfony\Collector\CollectOnFormVariableMethodCallsCollector; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\PhpParser\Printer\BetterStandardPrinter; use Rector\Rector\AbstractRector; use Rector\RectorDefinition\CodeSample; use Rector\RectorDefinition\RectorDefinition; @@ -54,19 +53,12 @@ final class FormControlToControllerAndFormTypeRector extends AbstractRector */ private $classNaming; - /** - * @var BetterStandardPrinter - */ - private $betterStandardPrinter; - public function __construct( CollectOnFormVariableMethodCallsCollector $collectOnFormVariableMethodCallsCollector, - ClassNaming $classNaming, - BetterStandardPrinter $betterStandardPrinter + ClassNaming $classNaming ) { $this->collectOnFormVariableMethodCallsCollector = $collectOnFormVariableMethodCallsCollector; $this->classNaming = $classNaming; - $this->betterStandardPrinter = $betterStandardPrinter; } public function getDefinition(): RectorDefinition @@ -269,7 +261,7 @@ PHP $filePath = dirname($fileInfo->getRealPath()) . DIRECTORY_SEPARATOR . 'SomeFormController.php'; // @todo make temporary - $content = 'betterStandardPrinter->print([$namespace]) . PHP_EOL; + $content = 'print([$namespace]) . PHP_EOL; FileSystem::write($filePath, $content); } diff --git a/packages/NetteToSymfony/tests/Rector/Assign/FormControlToControllerAndFormTypeRector/FormControlToControllerAndFormTypeRectorTest.php b/packages/NetteToSymfony/tests/Rector/Assign/FormControlToControllerAndFormTypeRector/FormControlToControllerAndFormTypeRectorTest.php index a9ac085e83d..b6149e98320 100644 --- a/packages/NetteToSymfony/tests/Rector/Assign/FormControlToControllerAndFormTypeRector/FormControlToControllerAndFormTypeRectorTest.php +++ b/packages/NetteToSymfony/tests/Rector/Assign/FormControlToControllerAndFormTypeRector/FormControlToControllerAndFormTypeRectorTest.php @@ -4,18 +4,14 @@ declare(strict_types=1); namespace Rector\NetteToSymfony\Tests\Rector\Assign\FormControlToControllerAndFormTypeRector; -use Iterator; use Rector\NetteToSymfony\Rector\Assign\FormControlToControllerAndFormTypeRector; use Rector\Testing\PHPUnit\AbstractRectorTestCase; final class FormControlToControllerAndFormTypeRectorTest extends AbstractRectorTestCase { - /** - * @dataProvider provideDataForTest() - */ - public function test(string $file): void + public function test(): void { - $this->doTestFile($file); + $this->doTestFile(__DIR__ . '/Fixture/fixture.php.inc'); $controllerFilePath = sys_get_temp_dir() . '/rector_temp_tests/SomeFormController.php'; $this->assertFileExists($controllerFilePath); @@ -23,11 +19,6 @@ final class FormControlToControllerAndFormTypeRectorTest extends AbstractRectorT $this->assertFileEquals(__DIR__ . '/Source/SomeFormController.php', $controllerFilePath); } - public function provideDataForTest(): Iterator - { - return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); - } - protected function getRectorClass(): string { return FormControlToControllerAndFormTypeRector::class; diff --git a/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php b/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php index 4a70f23f257..c3d732ea046 100644 --- a/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php +++ b/packages/NodeTypeResolver/src/PhpDoc/NodeAnalyzer/DocBlockManipulator.php @@ -479,15 +479,10 @@ final class DocBlockManipulator PhpDocInfo $phpDocInfo, bool $shouldSkipEmptyLinesAbove = false ): bool { - // skip if has no doc comment - if ($node->getDocComment() === null) { - return false; - } - $phpDoc = $this->phpDocInfoPrinter->printFormatPreserving($phpDocInfo, $shouldSkipEmptyLinesAbove); if ($phpDoc !== '') { // no change, don't save it - if ($node->getDocComment()->getText() === $phpDoc) { + if ($node->getDocComment() && $node->getDocComment()->getText() === $phpDoc) { return false; } diff --git a/src/PhpParser/Node/Manipulator/ClassManipulator.php b/src/PhpParser/Node/Manipulator/ClassManipulator.php index e82374fe6dd..ceacd4eed38 100644 --- a/src/PhpParser/Node/Manipulator/ClassManipulator.php +++ b/src/PhpParser/Node/Manipulator/ClassManipulator.php @@ -440,6 +440,30 @@ final class ClassManipulator return []; } + public function hasInterface(Class_ $class, string $desiredInterface): bool + { + foreach ($class->implements as $implement) { + if (! $this->nameResolver->isName($implement, $desiredInterface)) { + continue; + } + + return true; + } + + return false; + } + + public function removeInterface(Class_ $class, string $desiredInterface): void + { + foreach ($class->implements as $implement) { + if (! $this->nameResolver->isName($implement, $desiredInterface)) { + continue; + } + + $this->nodeRemovingCommander->addNode($implement); + } + } + private function tryInsertBeforeFirstMethod(Class_ $classNode, Stmt $stmt): bool { foreach ($classNode->stmts as $key => $classStmt) { diff --git a/src/PhpParser/Node/NodeFactory.php b/src/PhpParser/Node/NodeFactory.php index 111ea921163..3bb6288ec4b 100644 --- a/src/PhpParser/Node/NodeFactory.php +++ b/src/PhpParser/Node/NodeFactory.php @@ -228,10 +228,12 @@ final class NodeFactory ); } - public function createPrivateProperty(string $name): Property + public function createStaticProtectedPropertyWithDefault(string $name, Node $node): Property { $propertyBuilder = $this->builderFactory->property($name); - $propertyBuilder->makePrivate(); + $propertyBuilder->makeProtected(); + $propertyBuilder->makeStatic(); + $propertyBuilder->setDefault($node); $property = $propertyBuilder->getNode(); @@ -240,12 +242,10 @@ final class NodeFactory return $property; } - public function createStaticProtectedPropertyWithDefault(string $name, Node $node): Property + public function createPrivateProperty(string $name): Property { $propertyBuilder = $this->builderFactory->property($name); - $propertyBuilder->makeProtected(); - $propertyBuilder->makeStatic(); - $propertyBuilder->setDefault($node); + $propertyBuilder->makePrivate(); $property = $propertyBuilder->getNode(); diff --git a/src/Rector/AbstractRector/BetterStandardPrinterTrait.php b/src/Rector/AbstractRector/BetterStandardPrinterTrait.php index bdddcf88210..3b9576a6a8a 100644 --- a/src/Rector/AbstractRector/BetterStandardPrinterTrait.php +++ b/src/Rector/AbstractRector/BetterStandardPrinterTrait.php @@ -22,7 +22,7 @@ trait BetterStandardPrinterTrait /** * @var BetterStandardPrinter */ - private $betterStandardPrinter; + protected $betterStandardPrinter; /** * @required diff --git a/stubs/Gedmo/Mapping/Annotation/Locale.php b/stubs/Gedmo/Mapping/Annotation/Locale.php new file mode 100644 index 00000000000..18c7e291f70 --- /dev/null +++ b/stubs/Gedmo/Mapping/Annotation/Locale.php @@ -0,0 +1,16 @@ +