Skip to content

Commit dbbc516

Browse files
committed
[make:decorator] Add new maker to create decorator
#1401
1 parent ce60831 commit dbbc516

39 files changed

+1733
-9
lines changed

config/help/MakeDecorator.txt

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
The <info>%command.name%</info> command generates a new service decorator class.
2+
3+
<info>php %command.full_name%</info>
4+
<info>php %command.full_name% My\Decorated\Service\Class</info>
5+
<info>php %command.full_name% My\Decorated\Service\Class MyServiceDecorator</info>
6+
<info>php %command.full_name% My\Decorated\Service\Class Service\MyServiceDecorator</info>
7+
<info>php %command.full_name% my_decorated.service.id MyServiceDecorator</info>
8+
<info>php %command.full_name% my_decorated.service.id MyServiceDecorator</info>
9+
10+
If one argument is missing, the command will ask for it interactively.

config/makers.xml

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
<tag name="maker.command" />
3636
</service>
3737

38+
<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 -->
41+
<tag name="maker.command" />
42+
</service>
43+
3844
<service id="maker.maker.make_docker_database" class="Symfony\Bundle\MakerBundle\Maker\MakeDockerDatabase">
3945
<argument type="service" id="maker.file_manager" />
4046
<tag name="maker.command" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\CompilerPass;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
18+
/**
19+
* @author Benjamin Georgeault <[email protected]>
20+
*/
21+
class MakeDecoratorPass implements CompilerPassInterface
22+
{
23+
public function process(ContainerBuilder $container): void
24+
{
25+
if (!$container->hasDefinition('maker.maker.make_decorator')) {
26+
return;
27+
}
28+
29+
$container->getDefinition('maker.maker.make_decorator')
30+
->replaceArgument(0, ServiceLocatorTagPass::register($container, $ids = $container->getServiceIds()))
31+
->replaceArgument(1, $ids)
32+
;
33+
}
34+
}

src/Maker/MakeDecorator.php

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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\Maker;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
16+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
17+
use Symfony\Bundle\MakerBundle\Generator;
18+
use Symfony\Bundle\MakerBundle\InputConfiguration;
19+
use Symfony\Bundle\MakerBundle\Str;
20+
use Symfony\Bundle\MakerBundle\Util\DecoratorInfo;
21+
use Symfony\Bundle\MakerBundle\Validator;
22+
use Symfony\Component\Console\Command\Command;
23+
use Symfony\Component\Console\Input\InputArgument;
24+
use Symfony\Component\Console\Input\InputInterface;
25+
use Symfony\Component\Console\Question\Question;
26+
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
27+
28+
/**
29+
* @author Benjamin Georgeault <[email protected]>
30+
*/
31+
final class MakeDecorator extends AbstractMaker
32+
{
33+
/**
34+
* @param array<string> $ids
35+
*/
36+
public function __construct(
37+
private readonly ContainerInterface $container,
38+
private readonly array $ids,
39+
) {
40+
}
41+
42+
public static function getCommandName(): string
43+
{
44+
return 'make:decorator';
45+
}
46+
47+
public static function getCommandDescription(): string
48+
{
49+
return 'Create CRUD for Doctrine entity class';
50+
}
51+
52+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
53+
{
54+
$command
55+
->addArgument('id', InputArgument::OPTIONAL, 'The ID of the service to decorate.')
56+
->addArgument('decorator-class', InputArgument::OPTIONAL, \sprintf('The class name of the service to create (e.g. <fg=yellow>%sDecorator</>)', Str::asClassName(Str::getRandomTerm())))
57+
->setHelp($this->getHelpFileContents('MakeDecorator.txt'))
58+
;
59+
}
60+
61+
public function configureDependencies(DependencyBuilder $dependencies): void
62+
{
63+
$dependencies->addClassDependency(
64+
AsDecorator::class,
65+
'dependency-injection',
66+
);
67+
}
68+
69+
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
70+
{
71+
// Ask for service id.
72+
if (null === $input->getArgument('id')) {
73+
$argument = $command->getDefinition()->getArgument('id');
74+
75+
($question = new Question($argument->getDescription()))
76+
->setAutocompleterValues($this->ids)
77+
->setValidator(fn ($answer) => Validator::serviceExists($answer, $this->ids))
78+
->setMaxAttempts(3);
79+
80+
$input->setArgument('id', $io->askQuestion($question));
81+
}
82+
83+
$id = $input->getArgument('id');
84+
85+
// Ask for decorator classname.
86+
if (null === $input->getArgument('decorator-class')) {
87+
$argument = $command->getDefinition()->getArgument('decorator-class');
88+
89+
$basename = Str::getShortClassName(match (true) {
90+
interface_exists($id) => Str::removeSuffix($id, 'Interface'),
91+
class_exists($id) => $id,
92+
default => Str::asClassName($id),
93+
});
94+
95+
$defaultClass = Str::asClassName(\sprintf('%s Decorator', $basename));
96+
97+
($question = new Question($argument->getDescription(), $defaultClass))
98+
->setValidator(fn ($answer) => Validator::validateClassName(Validator::classDoesNotExist($answer)))
99+
->setMaxAttempts(3);
100+
101+
$input->setArgument('decorator-class', $io->askQuestion($question));
102+
}
103+
}
104+
105+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
106+
{
107+
$id = $input->getArgument('id');
108+
109+
$classNameDetails = $generator->createClassNameDetails(
110+
Validator::validateClassName(Validator::classDoesNotExist($input->getArgument('decorator-class'))),
111+
'',
112+
);
113+
114+
$decoratedInfo = $this->createDecoratorInfo($id, $classNameDetails->getFullName());
115+
$classData = $decoratedInfo->getClassData();
116+
117+
$generator->generateClassFromClassData(
118+
$classData,
119+
'decorator/Decorator.tpl.php',
120+
[
121+
'decorated_info' => $decoratedInfo,
122+
],
123+
);
124+
125+
$generator->writeChanges();
126+
127+
$this->writeSuccessMessage($io);
128+
}
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+
}
141+
}

src/MakerBundle.php

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bundle\MakerBundle;
1313

1414
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass;
15+
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeDecoratorPass;
1516
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\RemoveMissingParametersPass;
1617
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\SetDoctrineAnnotatedPrefixesPass;
1718
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
@@ -69,6 +70,7 @@ public function build(ContainerBuilder $container): void
6970
{
7071
// add a priority so we run before the core command pass
7172
$container->addCompilerPass(new MakeCommandRegistrationPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
73+
$container->addCompilerPass(new MakeDecoratorPass());
7274
$container->addCompilerPass(new RemoveMissingParametersPass());
7375
$container->addCompilerPass(new SetDoctrineAnnotatedPrefixesPass());
7476
}

src/Util/ClassSource/Model/ClassData.php

+36-8
Original file line numberDiff line numberDiff line change
@@ -30,33 +30,44 @@ private function __construct(
3030
private bool $isFinal = true,
3131
private string $rootNamespace = 'App',
3232
private ?string $classSuffix = null,
33+
public readonly ?array $implements = null,
3334
) {
3435
if (str_starts_with(haystack: $this->namespace, needle: $this->rootNamespace)) {
3536
$this->namespace = substr_replace(string: $this->namespace, replace: '', offset: 0, length: \strlen($this->rootNamespace) + 1);
3637
}
3738
}
3839

39-
public static function create(string $class, ?string $suffix = null, ?string $extendsClass = null, bool $isEntity = false, array $useStatements = []): self
40+
public static function create(string $class, ?string $suffix = null, ?string $extendsClass = null, bool $isEntity = false, array $useStatements = [], ?array $implements = null): self
4041
{
4142
$className = Str::getShortClassName($class);
4243

4344
if (null !== $suffix && !str_ends_with($className, $suffix)) {
4445
$className = Str::asClassName(\sprintf('%s%s', $className, $suffix));
4546
}
4647

47-
$useStatements = new UseStatementGenerator($useStatements);
48+
$className = Str::asClassName($className);
49+
50+
$useStatements = new UseStatementGenerator($useStatements, [$className]);
4851

4952
if ($extendsClass) {
50-
$useStatements->addUseStatement($extendsClass);
53+
$useStatements->addUseStatement($extendsClass, 'Base');
54+
}
55+
56+
if ($implements) {
57+
array_walk($implements, function (string &$interface) use ($useStatements) {
58+
$useStatements->addUseStatement($interface, 'Base');
59+
$interface = $useStatements->getShortName($interface);
60+
});
5161
}
5262

5363
return new self(
54-
className: Str::asClassName($className),
64+
className: $className,
5565
namespace: Str::getNamespace($class),
56-
extends: null === $extendsClass ? null : Str::getShortClassName($extendsClass),
66+
extends: null === $extendsClass ? null : $useStatements->getShortName($extendsClass),
5767
isEntity: $isEntity,
5868
useStatementGenerator: $useStatements,
5969
classSuffix: $suffix,
70+
implements: $implements,
6071
);
6172
}
6273

@@ -130,10 +141,17 @@ public function getClassDeclaration(): string
130141
$extendsDeclaration = \sprintf(' extends %s', $this->extends);
131142
}
132143

133-
return \sprintf('%sclass %s%s',
144+
$implementsDeclaration = '';
145+
146+
if (null !== $this->implements) {
147+
$implementsDeclaration = \sprintf(' implements %s', implode(', ', $this->implements));
148+
}
149+
150+
return \sprintf('%sclass %s%s%s',
134151
$this->isFinal ? 'final ' : '',
135152
$this->className,
136153
$extendsDeclaration,
154+
$implementsDeclaration,
137155
);
138156
}
139157

@@ -144,9 +162,9 @@ public function setIsFinal(bool $isFinal): self
144162
return $this;
145163
}
146164

147-
public function addUseStatement(array|string $useStatement): self
165+
public function addUseStatement(array|string $useStatement, ?string $aliasPrefixIfExist = null): self
148166
{
149-
$this->useStatementGenerator->addUseStatement($useStatement);
167+
$this->useStatementGenerator->addUseStatement($useStatement, $aliasPrefixIfExist);
150168

151169
return $this;
152170
}
@@ -155,4 +173,14 @@ public function getUseStatements(): string
155173
{
156174
return (string) $this->useStatementGenerator;
157175
}
176+
177+
public function getUseStatementShortName(string $className): string
178+
{
179+
return $this->useStatementGenerator->getShortName($className);
180+
}
181+
182+
public function hasUseStatement(string $className): bool
183+
{
184+
return $this->useStatementGenerator->hasUseStatement($className);
185+
}
158186
}
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Util\ClassSource\Model;
13+
14+
/**
15+
* @author Benjamin Georgeault <[email protected]>
16+
*
17+
* @internal
18+
*/
19+
final class ClassMethod
20+
{
21+
/**
22+
* @param MethodArgument[] $arguments
23+
*/
24+
public function __construct(
25+
private readonly string $name,
26+
private readonly array $arguments = [],
27+
private readonly ?string $returnType = null,
28+
private readonly bool $isStatic = false,
29+
) {
30+
}
31+
32+
public function getName(): string
33+
{
34+
return $this->name;
35+
}
36+
37+
public function isReturnVoid(): bool
38+
{
39+
return 'void' === $this->returnType;
40+
}
41+
42+
public function isStatic(): bool
43+
{
44+
return $this->isStatic;
45+
}
46+
47+
public function getDeclaration(): string
48+
{
49+
return \sprintf('public %sfunction %s(%s)%s',
50+
$this->isStatic ? 'static ' : '',
51+
$this->name,
52+
implode(', ', array_map(fn (MethodArgument $arg) => $arg->getDeclaration(), $this->arguments)),
53+
$this->returnType ? ': '.$this->returnType : '',
54+
);
55+
}
56+
57+
public function getArgumentsUse(): string
58+
{
59+
return implode(', ', array_map(fn (MethodArgument $arg) => $arg->getVariable(), $this->arguments));
60+
}
61+
}

0 commit comments

Comments
 (0)