Skip to content

Commit 71e21e6

Browse files
committed
Add formatter shorthand aliases for the Formatted validator
Introduce a FormattedSuffix transformer that decorates the existing Prefix transformer to support shorthand method names like maskFormatted(), patternFormatted(), and placeholderFormatted(). These expand internally to formatted(FormatterBuilder::mask(...), validator), removing the need to manually construct FormatterBuilder instances. The transformer intercepts specs whose name ends with "Formatted", extracts the formatter name from the prefix, pops the Validator from the second argument position, and delegates the remaining arguments to FormatterBuilder::__callStatic() to build the Formatter instance. This composes cleanly with the Prefix transformer, so combinations like notMaskFormatted() and keyPatternFormatted() work automatically. The LintMixinCommand is updated to scan formatter classes from the string-formatter package and generate corresponding alias methods across all mixin interfaces, following the same allow/deny list logic as the Formatted validator itself. Feature test expectations are also updated to match the current message templates. Assisted-by: Claude Code (claude-opus-4-5-20251101)
1 parent 42d28f3 commit 71e21e6

21 files changed

Lines changed: 650 additions & 14 deletions

src-dev/Commands/LintMixinCommand.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use ReflectionUnionType;
2323
use Respect\Dev\Differ\ConsoleDiffer;
2424
use Respect\Dev\Differ\Item;
25+
use Respect\StringFormatter\Formatter;
2526
use Respect\Validation\Mixins\AllBuilder;
2627
use Respect\Validation\Mixins\AllChain;
2728
use Respect\Validation\Mixins\Chain;
@@ -64,7 +65,10 @@
6465
use function preg_replace;
6566
use function sprintf;
6667
use function str_contains;
68+
use function str_ends_with;
6769
use function str_starts_with;
70+
use function strlen;
71+
use function substr;
6872
use function trim;
6973
use function ucfirst;
7074

@@ -207,6 +211,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int
207211
);
208212
}
209213

214+
$formatters = $this->scanFormatters();
215+
foreach ($formatters as [$formatterMethodName, $formatterReflection]) {
216+
$this->addFormatterAliasToInterface(
217+
$staticNamespace,
218+
$formatterMethodName,
219+
$staticInterface,
220+
$formatterReflection,
221+
$prefix,
222+
$allowList,
223+
$denyList,
224+
);
225+
$this->addFormatterAliasToInterface(
226+
$chainedNamespace,
227+
$formatterMethodName,
228+
$chainedInterface,
229+
$formatterReflection,
230+
$prefix,
231+
$allowList,
232+
$denyList,
233+
);
234+
}
235+
210236
$printer = new Printer();
211237
$printer->wrapLength = 300;
212238

@@ -273,6 +299,111 @@ private function scanValidators(string $directory): array
273299
return $names;
274300
}
275301

302+
/** @return array<array{string, ReflectionClass}> */
303+
private function scanFormatters(): array
304+
{
305+
$formatters = [];
306+
$formatterDir = dirname(__DIR__, 2) . '/vendor/respect/string-formatter/src';
307+
308+
foreach (new DirectoryIterator($formatterDir) as $file) {
309+
if (!$file->isFile()) {
310+
continue;
311+
}
312+
313+
$basename = $file->getBasename('.php');
314+
if (!str_ends_with($basename, 'Formatter') || $basename === 'FormatterBuilder') {
315+
continue;
316+
}
317+
318+
$className = 'Respect\\StringFormatter\\' . $basename;
319+
$reflection = new ReflectionClass($className);
320+
321+
if (!$reflection->implementsInterface(Formatter::class)) {
322+
continue;
323+
}
324+
325+
if ($reflection->isAbstract() || $reflection->getConstructor() === null) {
326+
continue;
327+
}
328+
329+
// e.g. MaskFormatter -> maskFormatted
330+
$shortName = $reflection->getShortName();
331+
$formatterName = lcfirst(substr($shortName, 0, -strlen('Formatter')));
332+
$methodName = $formatterName . 'Formatted';
333+
334+
$formatters[] = [$methodName, $reflection];
335+
}
336+
337+
return $formatters;
338+
}
339+
340+
/**
341+
* @param array<string> $allowList
342+
* @param array<string> $denyList
343+
*/
344+
private function addFormatterAliasToInterface(
345+
PhpNamespace $namespace,
346+
string $methodName,
347+
InterfaceType $interfaceType,
348+
ReflectionClass $formatterReflection,
349+
string|null $prefix,
350+
array $allowList,
351+
array $denyList,
352+
): void {
353+
// Apply same allow/deny logic as the Formatted validator
354+
if ($allowList !== [] && !in_array('Formatted', $allowList, true)) {
355+
return;
356+
}
357+
358+
if ($denyList !== [] && in_array('Formatted', $denyList, true)) {
359+
return;
360+
}
361+
362+
$name = $prefix ? $prefix . ucfirst($methodName) : $methodName;
363+
$method = $interfaceType->addMethod($name)->setPublic()->setReturnType(Chain::class);
364+
365+
if (str_contains($interfaceType->getName(), 'Builder')) {
366+
$method->setStatic();
367+
}
368+
369+
if ($prefix === 'key') {
370+
$method->addParameter('key')->setType('int|string');
371+
}
372+
373+
if ($prefix === 'property') {
374+
$method->addParameter('propertyName')->setType('string');
375+
}
376+
377+
$reflectionConstructor = $formatterReflection->getConstructor();
378+
379+
$comment = $reflectionConstructor->getDocComment();
380+
if ($comment) {
381+
$method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment));
382+
}
383+
384+
$formatterParams = $reflectionConstructor->getParameters();
385+
386+
// Add formatter constructor params, inserting Validator as 2nd parameter
387+
// The first parameter must always be mandatory
388+
$validatorInserted = false;
389+
foreach ($formatterParams as $index => $reflectionParameter) {
390+
if ($index === 1 && !$validatorInserted) {
391+
$namespace->addUse(Validator::class);
392+
$method->addParameter('validator')->setType(Validator::class);
393+
$validatorInserted = true;
394+
}
395+
396+
$this->addParameterToMethod($method, $reflectionParameter, $namespace, $index === 0);
397+
}
398+
399+
if ($validatorInserted) {
400+
return;
401+
}
402+
403+
$namespace->addUse(Validator::class);
404+
$method->addParameter('validator')->setType(Validator::class);
405+
}
406+
276407
/**
277408
* @param array<string> $allowList
278409
* @param array<string> $denyList
@@ -328,6 +459,7 @@ private function addParameterToMethod(
328459
Method $method,
329460
ReflectionParameter $reflectionParameter,
330461
PhpNamespace $namespace,
462+
bool $forceMandatory = false,
331463
): void {
332464
if ($reflectionParameter->isVariadic()) {
333465
$method->setVariadic();
@@ -363,6 +495,10 @@ private function addParameterToMethod(
363495
$parameter = $method->addParameter($reflectionParameter->getName());
364496
$parameter->setType(implode('|', $types));
365497

498+
if ($forceMandatory) {
499+
return;
500+
}
501+
366502
if (!$reflectionParameter->isDefaultValueAvailable()) {
367503
$parameter->setNullable($reflectionParameter->isOptional());
368504
}

src/ContainerRegistry.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
use Respect\Validation\Message\Parameters\PathHandler;
3939
use Respect\Validation\Message\Parameters\ResultHandler;
4040
use Respect\Validation\Message\Renderer;
41-
use Respect\Validation\Transformers\Prefix;
41+
use Respect\Validation\Transformers\FormattedSuffixTransformer;
42+
use Respect\Validation\Transformers\PrefixTransformer;
4243
use Respect\Validation\Transformers\Transformer;
4344
use Symfony\Contracts\Translation\TranslatorInterface;
4445

@@ -55,7 +56,7 @@ public static function createContainer(array $definitions = []): Container
5556
{
5657
return new Container($definitions + [
5758
PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()),
58-
Transformer::class => create(Prefix::class),
59+
Transformer::class => factory(static fn() => new FormattedSuffixTransformer(new PrefixTransformer())),
5960
TemplateResolver::class => create(TemplateResolver::class),
6061
TranslatorInterface::class => autowire(BypassTranslator::class),
6162
Renderer::class => autowire(InterpolationRenderer::class),

src/Mixins/AllBuilder.php

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Mixins/AllChain.php

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Mixins/Builder.php

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Mixins/Chain.php

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Mixins/KeyBuilder.php

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)