Skip to content

Commit 2f170a1

Browse files
authored
Validation (#49)
* Added ValidationAnnotation extractor * Added extractor for "invalid message" * Applied changes from StyleCI * Removed legacy code * Minor
1 parent 96bb84a commit 2f170a1

File tree

7 files changed

+325
-0
lines changed

7 files changed

+325
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"require-dev": {
1919
"phpunit/phpunit": "^4.8.36 || ^5.5 || ^6.2",
2020
"symfony/translation": "^3.0",
21+
"symfony/validator": "^2.7 || ^3.0",
2122
"symfony/twig-bridge": "^3.0"
2223
},
2324
"autoload": {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Extractor\Visitor\Php\Symfony;
13+
14+
use PhpParser\Node;
15+
use PhpParser\Node\Stmt;
16+
use PhpParser\NodeVisitor;
17+
use Translation\Extractor\Model\SourceLocation;
18+
use Translation\Extractor\Visitor\Php\BasePHPVisitor;
19+
20+
/**
21+
* @author Tobias Nyholm <[email protected]>
22+
*/
23+
final class FormTypeInvalidMessage extends BasePHPVisitor implements NodeVisitor
24+
{
25+
public function enterNode(Node $node)
26+
{
27+
// only Traverse *Type
28+
if ($node instanceof Stmt\Class_) {
29+
if (substr($node->name, -4) !== 'Type') {
30+
return;
31+
}
32+
}
33+
34+
if (!$node instanceof Node\Expr\Array_) {
35+
return;
36+
}
37+
38+
foreach ($node->items as $item) {
39+
if (!$item->key instanceof Node\Scalar\String_) {
40+
continue;
41+
}
42+
43+
if ($item->key->value !== 'invalid_message') {
44+
continue;
45+
}
46+
47+
if (!$item->value instanceof Node\Scalar\String_) {
48+
$this->addError($item, 'Form label is not a scalar string');
49+
50+
continue;
51+
}
52+
53+
$label = $item->value->value;
54+
if (empty($label)) {
55+
continue;
56+
}
57+
58+
$sl = new SourceLocation($label, $this->getAbsoluteFilePath(), $node->getAttribute('startLine'), ['domain' => 'validators']);
59+
$this->collection->addLocation($sl);
60+
}
61+
}
62+
63+
public function leaveNode(Node $node)
64+
{
65+
}
66+
67+
public function beforeTraverse(array $nodes)
68+
{
69+
}
70+
71+
public function afterTraverse(array $nodes)
72+
{
73+
}
74+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Extractor\Visitor\Php\Symfony;
13+
14+
use PhpParser\Node;
15+
use PhpParser\NodeVisitor;
16+
use Translation\Extractor\Model\SourceLocation;
17+
use Translation\Extractor\Visitor\Php\BasePHPVisitor;
18+
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
19+
use Symfony\Component\Validator\MetadataFactoryInterface as LegacyMetadataFactoryInterface;
20+
21+
/**
22+
* @author Tobias Nyholm <[email protected]>
23+
*/
24+
final class ValidationAnnotation extends BasePHPVisitor implements NodeVisitor
25+
{
26+
/**
27+
* @var MetadataFactoryInterface|LegacyMetadataFactoryInterface
28+
*/
29+
private $metadataFactory;
30+
31+
/**
32+
* @var string
33+
*/
34+
private $namespace;
35+
36+
/**
37+
* ValidationExtractor constructor.
38+
*
39+
* @param MetadataFactoryInterface|LegacyMetadataFactoryInterface $metadataFactory
40+
*/
41+
public function __construct($metadataFactory)
42+
{
43+
if (!(
44+
$metadataFactory instanceof MetadataFactoryInterface
45+
|| $metadataFactory instanceof LegacyMetadataFactoryInterface
46+
)) {
47+
throw new \InvalidArgumentException(sprintf('%s expects an instance of MetadataFactoryInterface', get_class($this)));
48+
}
49+
$this->metadataFactory = $metadataFactory;
50+
}
51+
52+
public function beforeTraverse(array $nodes)
53+
{
54+
$this->namespace = '';
55+
}
56+
57+
public function enterNode(Node $node)
58+
{
59+
if ($node instanceof Node\Stmt\Namespace_) {
60+
if (isset($node->name)) {
61+
// save the namespace
62+
$this->namespace = implode('\\', $node->name->parts);
63+
}
64+
65+
return;
66+
}
67+
68+
if (!$node instanceof Node\Stmt\Class_) {
69+
return;
70+
}
71+
72+
$name = '' === $this->namespace ? $node->name : $this->namespace.'\\'.$node->name;
73+
74+
if (!class_exists($name)) {
75+
return;
76+
}
77+
78+
$metadata = $this->metadataFactory->getMetadataFor($name);
79+
if (!$metadata->hasConstraints() && !count($metadata->getConstrainedProperties())) {
80+
return;
81+
}
82+
83+
$this->extractFromConstraints($metadata->constraints);
84+
foreach ($metadata->members as $members) {
85+
foreach ($members as $member) {
86+
$this->extractFromConstraints($member->constraints);
87+
}
88+
}
89+
}
90+
91+
/**
92+
* @param array $constraints
93+
*/
94+
private function extractFromConstraints(array $constraints)
95+
{
96+
foreach ($constraints as $constraint) {
97+
$ref = new \ReflectionClass($constraint);
98+
$defaultValues = $ref->getDefaultProperties();
99+
100+
$properties = $ref->getProperties();
101+
102+
foreach ($properties as $property) {
103+
$propName = $property->getName();
104+
105+
// If the property ends with 'Message'
106+
if (strtolower(substr($propName, -1 * strlen('Message'))) === 'message') {
107+
// If it is different from the default value
108+
if ($defaultValues[$propName] !== $constraint->{$propName}) {
109+
$this->collection->addLocation(new SourceLocation($constraint->{$propName}, $this->getAbsoluteFilePath(), 0, ['domain' => 'validators']));
110+
}
111+
}
112+
}
113+
}
114+
}
115+
116+
public function leaveNode(Node $node)
117+
{
118+
}
119+
120+
public function afterTraverse(array $nodes)
121+
{
122+
}
123+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\extractor\tests\Functional\Visitor\Php\Symfony;
13+
14+
use Translation\Extractor\Tests\Functional\Visitor\Php\BasePHPVisitorTest;
15+
use Translation\Extractor\Tests\Resources;
16+
use Translation\Extractor\Visitor\Php\Symfony\FormTypeInvalidMessage;
17+
18+
/**
19+
* @author Tobias Nyholm <[email protected]>
20+
*/
21+
class FormTypeInvalidMessageTest extends BasePHPVisitorTest
22+
{
23+
public function testExtract()
24+
{
25+
$collection = $this->getSourceLocations(new FormTypeInvalidMessage(), Resources\Php\Symfony\FormInvalidMessage::class);
26+
27+
$found = false;
28+
foreach ($collection as $source) {
29+
if ('password.not_match' === $source->getMessage()) {
30+
$this->assertEquals('validators', $source->getContext()['domain']);
31+
$found = true;
32+
}
33+
}
34+
35+
$this->assertTrue($found, 'We must be able to extract the form\'s "invalid_message".');
36+
}
37+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Extractor\Tests\Functional\Visitor\Php\Symfony;
13+
14+
use Translation\Extractor\Tests\Functional\Visitor\Php\BasePHPVisitorTest;
15+
use Translation\Extractor\Tests\Resources;
16+
use Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation;
17+
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
18+
use Doctrine\Common\Annotations\AnnotationReader;
19+
20+
/**
21+
* @author Tobias Nyholm <[email protected]>
22+
*/
23+
final class ValidationAnnotationTest extends BasePHPVisitorTest
24+
{
25+
public function testExtractAnnotation()
26+
{
27+
//use correct factory class depending on whether using Symfony 2 or 3
28+
if (class_exists('Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory')) {
29+
$metadataFactoryClass = 'Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory';
30+
} else {
31+
$metadataFactoryClass = 'Symfony\Component\Validator\Mapping\ClassMetadataFactory';
32+
}
33+
34+
$factory = new $metadataFactoryClass(new AnnotationLoader(new AnnotationReader()));
35+
$extractor = new ValidationAnnotation($factory);
36+
$collection = $this->getSourceLocations($extractor, Resources\Php\Symfony\ValidatorAnnotation::class);
37+
38+
$this->assertCount(2, $collection);
39+
$source = $collection->get(0);
40+
$this->assertEquals('start.null', $source->getMessage());
41+
$this->assertEquals('validators', $source->getContext()['domain']);
42+
43+
$source = $collection->get(1);
44+
$this->assertEquals('end.blank', $source->getMessage());
45+
$this->assertEquals('validators', $source->getContext()['domain']);
46+
}
47+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Translation\extractor\tests\Resources\Php\Symfony;
4+
5+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
6+
use Symfony\Component\Form\AbstractType;
7+
use Symfony\Component\Form\FormBuilderInterface;
8+
9+
class FormInvalidMessage extends AbstractType
10+
{
11+
public function buildForm(FormBuilderInterface $builder, array $options)
12+
{
13+
$builder->add('password', RepeatedType::class, array(
14+
'type' => 'password',
15+
'first_name' => 'new',
16+
'second_name' => 'confirm',
17+
'first_options' => array('label' => 'label.password.new'),
18+
'second_options' => array('label' => 'label.password.confirm'),
19+
'invalid_message' => 'password.not_match', // <--- This message
20+
'options' => array(),
21+
'required' => true,
22+
));
23+
}
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Translation\Extractor\Tests\Resources\Php\Symfony;
4+
5+
use Symfony\Component\Validator\Constraints as Assert;
6+
use Symfony\Component\Validator\Constraints\NotBlank;
7+
8+
class ValidatorAnnotation
9+
{
10+
/**
11+
* @Assert\NotNull(message="start.null")
12+
*/
13+
private $start;
14+
15+
/**
16+
* @NotBlank(message="end.blank")
17+
*/
18+
private $end;
19+
}

0 commit comments

Comments
 (0)