Skip to content

Commit 478542f

Browse files
committed
Add DecoratorHelper + Suggest service's id on error/shortname + priority/onInvalid
Also fix: - ignore `__construct` - fix static method on decorate by implements - Fix description
1 parent 2fb5feb commit 478542f

File tree

10 files changed

+419
-51
lines changed

10 files changed

+419
-51
lines changed

config/makers.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
</service>
3737

3838
<service id="maker.maker.make_decorator" class="Symfony\Bundle\MakerBundle\Maker\MakeDecorator">
39-
<argument /> <!-- Service locator of all existing services -->
40-
<argument /> <!-- Array of services' ids -->
39+
<argument type="service" id="maker.decorator_helper" />
4140
<tag name="maker.command" />
4241
</service>
4342

config/services.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
<argument type="service" id="doctrine" on-invalid="ignore" />
4141
</service>
4242

43+
<service id="maker.decorator_helper" class="Symfony\Bundle\MakerBundle\DependencyInjection\DecoratorHelper">
44+
<argument /> <!-- Service locator -->
45+
<argument /> <!-- Services ids -->
46+
<argument /> <!-- ShortName map of services -->
47+
</service>
48+
4349
<service id="maker.template_linter" class="Symfony\Bundle\MakerBundle\Util\TemplateLinter">
4450
<argument type="service" id="maker.file_manager" />
4551
<argument>%env(default::string:MAKER_PHP_CS_FIXER_BINARY_PATH)%</argument>

src/DependencyInjection/CompilerPass/MakeDecoratorPass.php

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass;
1313

14+
use Symfony\Bundle\MakerBundle\Str;
1415
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15-
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717

1818
/**
@@ -22,13 +22,45 @@ class MakeDecoratorPass implements CompilerPassInterface
2222
{
2323
public function process(ContainerBuilder $container): void
2424
{
25-
if (!$container->hasDefinition('maker.maker.make_decorator')) {
25+
if (!$container->hasDefinition('maker.decorator_helper')) {
2626
return;
2727
}
2828

29-
$container->getDefinition('maker.maker.make_decorator')
30-
->replaceArgument(0, ServiceLocatorTagPass::register($container, $ids = $container->getServiceIds()))
31-
->replaceArgument(1, $ids)
29+
$shortNameMap = [];
30+
$serviceClasses = [];
31+
foreach ($container->getServiceIds() as $id) {
32+
if (str_starts_with($id, '.')) {
33+
continue;
34+
}
35+
36+
if (interface_exists($id) || class_exists($id)) {
37+
$shortClass = Str::getShortClassName($id);
38+
$shortNameMap[$shortClass][] = $id;
39+
}
40+
41+
if (!$container->hasDefinition($id)) {
42+
continue;
43+
}
44+
45+
if (
46+
(null === $class = $container->getDefinition($id)->getClass())
47+
|| $class === $id
48+
) {
49+
continue;
50+
}
51+
52+
$shortClass = Str::getShortClassName($class);
53+
$shortNameMap[$shortClass][] = $id;
54+
$serviceClasses[$id] = $class;
55+
}
56+
57+
$shortNameMap = array_map(array_unique(...), $shortNameMap);
58+
59+
$ids = $container->getServiceIds();
60+
$container->getDefinition('maker.decorator_helper')
61+
->replaceArgument(0, $ids)
62+
->replaceArgument(1, $serviceClasses)
63+
->replaceArgument(2, $shortNameMap)
3264
;
3365
}
3466
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony MakerBundle package.
5+
*
6+
* (c) Fabien Potencier <[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 Symfony\Bundle\MakerBundle\DependencyInjection;
13+
14+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
15+
16+
/**
17+
* @author Benjamin Georgeault <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
final class DecoratorHelper
22+
{
23+
/**
24+
* @param array<string> $ids
25+
* @param array<string, string> $serviceClasses
26+
* @param array<string, string[]> $shortNameMap
27+
*/
28+
public function __construct(
29+
private readonly array $ids,
30+
private readonly array $serviceClasses,
31+
private readonly array $shortNameMap,
32+
) {
33+
}
34+
35+
public function suggestIds(): array
36+
{
37+
return [
38+
...array_keys($this->shortNameMap),
39+
...$this->ids,
40+
];
41+
}
42+
43+
public function getRealId(string $id): ?string
44+
{
45+
if (\in_array($id, $this->ids)) {
46+
return $id;
47+
}
48+
49+
if (\array_key_exists($id, $this->shortNameMap) && 1 === \count($this->shortNameMap[$id])) {
50+
return $this->shortNameMap[$id][0];
51+
}
52+
53+
return null;
54+
}
55+
56+
public function guessRealIds(string $id): array
57+
{
58+
$guessTypos = [];
59+
foreach ($this->shortNameMap as $shortName => $ids) {
60+
if (levenshtein($id, $shortName) < 3) {
61+
$guessTypos = [
62+
...$guessTypos,
63+
...$ids,
64+
];
65+
}
66+
}
67+
68+
foreach ($this->ids as $suggestId) {
69+
if (levenshtein($id, $suggestId) < 3) {
70+
$guessTypos[] = $suggestId;
71+
}
72+
}
73+
74+
return $guessTypos;
75+
}
76+
77+
/**
78+
* @return class-string
79+
*/
80+
public function getClass(string $id): string
81+
{
82+
if (class_exists($id) || interface_exists($id)) {
83+
return $id;
84+
}
85+
86+
if (\array_key_exists($id, $this->serviceClasses)) {
87+
return $this->serviceClasses[$id];
88+
}
89+
90+
throw new RuntimeCommandException(\sprintf('Cannot getClass for id "%s".', $id));
91+
}
92+
}

src/Maker/MakeDecorator.php

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212
namespace Symfony\Bundle\MakerBundle\Maker;
1313

14-
use Psr\Container\ContainerInterface;
1514
use Symfony\Bundle\MakerBundle\ConsoleStyle;
1615
use Symfony\Bundle\MakerBundle\DependencyBuilder;
16+
use Symfony\Bundle\MakerBundle\DependencyInjection\DecoratorHelper;
17+
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
1718
use Symfony\Bundle\MakerBundle\Generator;
1819
use Symfony\Bundle\MakerBundle\InputConfiguration;
1920
use Symfony\Bundle\MakerBundle\Str;
@@ -22,6 +23,8 @@
2223
use Symfony\Component\Console\Command\Command;
2324
use Symfony\Component\Console\Input\InputArgument;
2425
use Symfony\Component\Console\Input\InputInterface;
26+
use Symfony\Component\Console\Input\InputOption;
27+
use Symfony\Component\Console\Question\ConfirmationQuestion;
2528
use Symfony\Component\Console\Question\Question;
2629
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
2730

@@ -30,12 +33,8 @@
3033
*/
3134
final class MakeDecorator extends AbstractMaker
3235
{
33-
/**
34-
* @param array<string> $ids
35-
*/
3636
public function __construct(
37-
private readonly ContainerInterface $container,
38-
private readonly array $ids,
37+
private readonly DecoratorHelper $helper,
3938
) {
4039
}
4140

@@ -46,16 +45,21 @@ public static function getCommandName(): string
4645

4746
public static function getCommandDescription(): string
4847
{
49-
return 'Create CRUD for Doctrine entity class';
48+
return 'Create a decorator of a service';
5049
}
5150

5251
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
5352
{
5453
$command
5554
->addArgument('id', InputArgument::OPTIONAL, 'The ID of the service to decorate.')
5655
->addArgument('decorator-class', InputArgument::OPTIONAL, \sprintf('The class name of the service to create (e.g. <fg=yellow>%sDecorator</>)', Str::asClassName(Str::getRandomTerm())))
56+
->addOption('priority', null, InputOption::VALUE_REQUIRED, 'The priority of this decoration when multiple decorators are declared for the same service.')
57+
->addOption('on-invalid', null, InputOption::VALUE_REQUIRED, 'The behavior to adopt when the decoration is invalid.')
5758
->setHelp($this->getHelpFileContents('MakeDecorator.txt'))
5859
;
60+
61+
$inputConfig->setArgumentAsNonInteractive('id');
62+
$inputConfig->setArgumentAsNonInteractive('decorator-class');
5963
}
6064

6165
public function configureDependencies(DependencyBuilder $dependencies): void
@@ -73,14 +77,36 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
7377
$argument = $command->getDefinition()->getArgument('id');
7478

7579
($question = new Question($argument->getDescription()))
76-
->setAutocompleterValues($this->ids)
77-
->setValidator(fn ($answer) => Validator::serviceExists($answer, $this->ids))
80+
->setAutocompleterValues($suggestIds = $this->helper->suggestIds())
81+
->setValidator(fn ($answer) => Validator::serviceExists($answer, $suggestIds))
7882
->setMaxAttempts(3);
7983

8084
$input->setArgument('id', $io->askQuestion($question));
8185
}
8286

8387
$id = $input->getArgument('id');
88+
if (null === $realId = $this->helper->getRealId($id)) {
89+
$guessCount = \count($guessRealIds = $this->helper->guessRealIds($id));
90+
91+
if (0 === $guessCount) {
92+
throw new RuntimeCommandException(\sprintf('Cannot find nor guess service for given id "%s".', $id));
93+
} elseif (1 === $guessCount) {
94+
$question = new ConfirmationQuestion(\sprintf('<fg=green>Did you mean</> <fg=yellow>"%s"</> <fg=green>?</>', $guessRealIds[0]), true);
95+
96+
if (!$io->askQuestion($question)) {
97+
throw new RuntimeCommandException(\sprintf('Cannot find nor guess service for given id "%s".', $id));
98+
}
99+
100+
$input->setArgument('id', $id = $guessRealIds[0]);
101+
} else {
102+
$input->setArgument(
103+
'id',
104+
$id = $io->choice(\sprintf('Multiple services found for "%s", choice which one you want to decorate?', $id), $guessRealIds),
105+
);
106+
}
107+
} else {
108+
$input->setArgument('id', $id = $realId);
109+
}
84110

85111
// Ask for decorator classname.
86112
if (null === $input->getArgument('decorator-class')) {
@@ -111,7 +137,17 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
111137
'',
112138
);
113139

114-
$decoratedInfo = $this->createDecoratorInfo($id, $classNameDetails->getFullName());
140+
$priority = $input->getOption('priority');
141+
$onInvalid = $input->getOption('on-invalid');
142+
143+
$decoratedInfo = new DecoratorInfo(
144+
$classNameDetails->getFullName(),
145+
$id,
146+
$this->helper->getClass($id),
147+
empty($priority) ? null : $priority,
148+
null === $onInvalid || 1 === $onInvalid ? null : $onInvalid,
149+
);
150+
115151
$classData = $decoratedInfo->getClassData();
116152

117153
$generator->generateClassFromClassData(
@@ -126,16 +162,4 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
126162

127163
$this->writeSuccessMessage($io);
128164
}
129-
130-
private function createDecoratorInfo(string $id, string $decoratorClass): DecoratorInfo
131-
{
132-
return new DecoratorInfo(
133-
$decoratorClass,
134-
match (true) {
135-
class_exists($id), interface_exists($id) => $id,
136-
default => $this->container->get($id)::class,
137-
},
138-
$id,
139-
);
140-
}
141165
}

src/Util/ClassSource/Model/ClassData.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,9 @@ public function hasUseStatement(string $className): bool
183183
{
184184
return $this->useStatementGenerator->hasUseStatement($className);
185185
}
186+
187+
public function hasExtends(): bool
188+
{
189+
return null !== $this->extends;
190+
}
186191
}

0 commit comments

Comments
 (0)