Skip to content

Commit 0f1a1a6

Browse files
authored
Merge pull request #160 from lukepass/master
New extractor for constraints
2 parents 53c9c8e + 0174c5e commit 0f1a1a6

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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\Visitor\Php\BasePHPVisitor;
17+
18+
/**
19+
* @author Luca Passini <[email protected]>
20+
*/
21+
final class Constraint extends BasePHPVisitor implements NodeVisitor
22+
{
23+
public const VALIDATORS_DOMAIN = 'validators';
24+
25+
public const CONSTRAINT_CLASS_NAMES = [
26+
'AbstractComparison',
27+
'All',
28+
'Bic',
29+
'Blank',
30+
'Callback',
31+
'CardScheme',
32+
'Choice',
33+
'Collection',
34+
'Composite',
35+
'Count',
36+
'Country',
37+
'Currency',
38+
'Date',
39+
'DateTime',
40+
'DisableAutoMapping',
41+
'DivisibleBy',
42+
'Email',
43+
'EnableAutoMapping',
44+
'EqualTo',
45+
'Existence',
46+
'Expression',
47+
'File',
48+
'GreaterThan',
49+
'GreaterThanOrEqual',
50+
'GroupSequence',
51+
'GroupSequenceProvider',
52+
'Iban',
53+
'IdenticalTo',
54+
'Image',
55+
'Ip',
56+
'Isbn',
57+
'IsFalse',
58+
'IsNull',
59+
'Issn',
60+
'IsTrue',
61+
'Json',
62+
'Language',
63+
'Length',
64+
'LessThan',
65+
'LessThanOrEqual',
66+
'Locale',
67+
'Luhn',
68+
'Negative',
69+
'NegativeOrZero',
70+
'NotBlank',
71+
'NotCompromisedPassword',
72+
'NotEqualTo',
73+
'NotIdenticalTo',
74+
'NotNull',
75+
'NumberConstraintTrait',
76+
'Optional',
77+
'Positive',
78+
'PositiveOrZero',
79+
'Range',
80+
'Regex',
81+
'Required',
82+
'Time',
83+
'Timezone',
84+
'Traverse',
85+
'Type',
86+
'Unique',
87+
'Url',
88+
'Uuid',
89+
'Valid',
90+
];
91+
92+
/**
93+
* {@inheritdoc}
94+
*/
95+
public function beforeTraverse(array $nodes): ?Node
96+
{
97+
return null;
98+
}
99+
100+
/**
101+
* {@inheritdoc}
102+
*/
103+
public function enterNode(Node $node): ?Node
104+
{
105+
if (!$node instanceof Node\Expr\New_) {
106+
return null;
107+
}
108+
109+
$className = $node->class;
110+
if (!$className instanceof Node\Name) {
111+
return null;
112+
}
113+
114+
$parts = $className->parts;
115+
$isConstraintClass = false;
116+
117+
// we need to check every part since `Assert\NotBlank` would be split in 2 different pieces
118+
foreach ($parts as $part) {
119+
if (\in_array($part, self::CONSTRAINT_CLASS_NAMES)) {
120+
$isConstraintClass = true;
121+
122+
break;
123+
}
124+
}
125+
126+
// unsupported class
127+
if (!$isConstraintClass) {
128+
return null;
129+
}
130+
131+
$args = $node->args;
132+
if (0 === \count($args)) {
133+
return null;
134+
}
135+
136+
$arg = $args[0];
137+
if (!$arg instanceof Node\Arg) {
138+
return null;
139+
}
140+
141+
$options = $arg->value;
142+
if (!$options instanceof Node\Expr\Array_) {
143+
return null;
144+
}
145+
146+
$message = null;
147+
$messageNode = null;
148+
149+
foreach ($options->items as $item) {
150+
if (!$item->key instanceof Node\Scalar\String_) {
151+
continue;
152+
}
153+
154+
// there could be false positives but it should catch most of the useful properties
155+
// (e.g. `message`, `minMessage`)
156+
if (false === stripos($item->key->value, 'message')) {
157+
continue;
158+
}
159+
160+
if (!$item->value instanceof Node\Scalar\String_) {
161+
$this->addError($item, 'Constraint message is not a scalar string');
162+
163+
continue;
164+
}
165+
166+
$message = $item->value->value;
167+
$messageNode = $item;
168+
169+
break;
170+
}
171+
172+
if (!empty($message) && null !== $messageNode) {
173+
$this->addLocation($message, $messageNode->getAttribute('startLine'), $messageNode, [
174+
'domain' => self::VALIDATORS_DOMAIN,
175+
]);
176+
}
177+
178+
return null;
179+
}
180+
181+
/**
182+
* {@inheritdoc}
183+
*/
184+
public function leaveNode(Node $node): ?Node
185+
{
186+
return null;
187+
}
188+
189+
/**
190+
* {@inheritdoc}
191+
*/
192+
public function afterTraverse(array $nodes): ?Node
193+
{
194+
return null;
195+
}
196+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\Constraint;
17+
18+
/**
19+
* @author Tobias Nyholm <[email protected]>
20+
*/
21+
final class ConstraintTest extends BasePHPVisitorTest
22+
{
23+
public function testExtract()
24+
{
25+
$collection = $this->getSourceLocations(new Constraint(), Resources\Php\Symfony\Constraint::class);
26+
27+
$this->assertCount(2, $collection);
28+
29+
$first = $collection->get(0);
30+
$this->assertEquals('error.custom_not_blank', $first->getMessage());
31+
32+
$second = $collection->get(1);
33+
$this->assertEquals('error.custom_length', $second->getMessage());
34+
}
35+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 Constraint
9+
{
10+
/**
11+
* @return array
12+
*/
13+
public function newAction()
14+
{
15+
$notBlankConstraint = new NotBlank([
16+
'message' => 'error.custom_not_blank',
17+
]);
18+
19+
$lengthConstraint = new Assert\Length([
20+
'min' => 6,
21+
'minMessage' => 'error.custom_length',
22+
]);
23+
24+
return [];
25+
}
26+
}

0 commit comments

Comments
 (0)