-
-
Notifications
You must be signed in to change notification settings - Fork 417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[make:decorator] Add new maker to create decorator #1613
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
The <info>%command.name%</info> command generates a new service decorator class. | ||
|
||
<info>php %command.full_name%</info> | ||
<info>php %command.full_name% My\Decorated\Service\Class</info> | ||
<info>php %command.full_name% My\Decorated\Service\Class MyServiceDecorator</info> | ||
<info>php %command.full_name% My\Decorated\Service\Class Service\MyServiceDecorator</info> | ||
<info>php %command.full_name% my_decorated.service.id MyServiceDecorator</info> | ||
<info>php %command.full_name% my_decorated.service.id MyServiceDecorator</info> | ||
|
||
If one argument is missing, the command will ask for it interactively. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony MakerBundle package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass; | ||
|
||
use Symfony\Bundle\MakerBundle\Str; | ||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
|
||
/** | ||
* @author Benjamin Georgeault <[email protected]> | ||
*/ | ||
class MakeDecoratorPass implements CompilerPassInterface | ||
{ | ||
public function process(ContainerBuilder $container): void | ||
{ | ||
if (!$container->hasDefinition('maker.decorator_helper')) { | ||
return; | ||
} | ||
|
||
$shortNameMap = []; | ||
$serviceClasses = []; | ||
foreach ($container->getServiceIds() as $id) { | ||
if (str_starts_with($id, '.')) { | ||
continue; | ||
} | ||
|
||
if (interface_exists($id) || class_exists($id)) { | ||
$shortClass = Str::getShortClassName($id); | ||
$shortNameMap[$shortClass][] = $id; | ||
} | ||
|
||
if (!$container->hasDefinition($id)) { | ||
continue; | ||
} | ||
|
||
if ( | ||
(null === $class = $container->getDefinition($id)->getClass()) | ||
|| $class === $id | ||
) { | ||
continue; | ||
} | ||
|
||
$shortClass = Str::getShortClassName($class); | ||
$shortNameMap[$shortClass][] = $id; | ||
$serviceClasses[$id] = $class; | ||
} | ||
|
||
$shortNameMap = array_map(array_unique(...), $shortNameMap); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you meant to use https://www.php.net/manual/en/function.array-walk.php instead ? |
||
|
||
$ids = $container->getServiceIds(); | ||
$container->getDefinition('maker.decorator_helper') | ||
->replaceArgument(0, $ids) | ||
->replaceArgument(1, $serviceClasses) | ||
->replaceArgument(2, $shortNameMap) | ||
; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,92 @@ | ||||||
<?php | ||||||
|
||||||
/* | ||||||
* This file is part of the Symfony MakerBundle package. | ||||||
* | ||||||
* (c) Fabien Potencier <[email protected]> | ||||||
* | ||||||
* For the full copyright and license information, please view the LICENSE | ||||||
* file that was distributed with this source code. | ||||||
*/ | ||||||
|
||||||
namespace Symfony\Bundle\MakerBundle\DependencyInjection; | ||||||
|
||||||
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; | ||||||
|
||||||
/** | ||||||
* @author Benjamin Georgeault <[email protected]> | ||||||
* | ||||||
* @internal | ||||||
*/ | ||||||
final class DecoratorHelper | ||||||
{ | ||||||
/** | ||||||
* @param array<string> $ids | ||||||
* @param array<string, string> $serviceClasses | ||||||
* @param array<string, string[]> $shortNameMap | ||||||
*/ | ||||||
public function __construct( | ||||||
private readonly array $ids, | ||||||
private readonly array $serviceClasses, | ||||||
private readonly array $shortNameMap, | ||||||
) { | ||||||
} | ||||||
|
||||||
public function suggestIds(): array | ||||||
{ | ||||||
return [ | ||||||
...array_keys($this->shortNameMap), | ||||||
...$this->ids, | ||||||
]; | ||||||
} | ||||||
|
||||||
public function getRealId(string $id): ?string | ||||||
{ | ||||||
if (\in_array($id, $this->ids)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
return $id; | ||||||
} | ||||||
|
||||||
if (\array_key_exists($id, $this->shortNameMap) && 1 === \count($this->shortNameMap[$id])) { | ||||||
return $this->shortNameMap[$id][0]; | ||||||
} | ||||||
|
||||||
return null; | ||||||
} | ||||||
|
||||||
public function guessRealIds(string $id): array | ||||||
{ | ||||||
$guessTypos = []; | ||||||
foreach ($this->shortNameMap as $shortName => $ids) { | ||||||
if (levenshtein($id, $shortName) < 3) { | ||||||
$guessTypos = [ | ||||||
...$guessTypos, | ||||||
...$ids, | ||||||
]; | ||||||
} | ||||||
} | ||||||
|
||||||
foreach ($this->ids as $suggestId) { | ||||||
if (levenshtein($id, $suggestId) < 3) { | ||||||
$guessTypos[] = $suggestId; | ||||||
} | ||||||
} | ||||||
|
||||||
return $guessTypos; | ||||||
} | ||||||
|
||||||
/** | ||||||
* @return class-string | ||||||
*/ | ||||||
public function getClass(string $id): string | ||||||
{ | ||||||
if (class_exists($id) || interface_exists($id)) { | ||||||
return $id; | ||||||
} | ||||||
|
||||||
if (\array_key_exists($id, $this->serviceClasses)) { | ||||||
return $this->serviceClasses[$id]; | ||||||
} | ||||||
|
||||||
throw new RuntimeCommandException(\sprintf('Cannot getClass for id "%s".', $id)); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,165 @@ | ||||||
<?php | ||||||
|
||||||
/* | ||||||
* This file is part of the Symfony MakerBundle package. | ||||||
* | ||||||
* (c) Fabien Potencier <[email protected]> | ||||||
* | ||||||
* For the full copyright and license information, please view the LICENSE | ||||||
* file that was distributed with this source code. | ||||||
*/ | ||||||
|
||||||
namespace Symfony\Bundle\MakerBundle\Maker; | ||||||
|
||||||
use Symfony\Bundle\MakerBundle\ConsoleStyle; | ||||||
use Symfony\Bundle\MakerBundle\DependencyBuilder; | ||||||
use Symfony\Bundle\MakerBundle\DependencyInjection\DecoratorHelper; | ||||||
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; | ||||||
use Symfony\Bundle\MakerBundle\Generator; | ||||||
use Symfony\Bundle\MakerBundle\InputConfiguration; | ||||||
use Symfony\Bundle\MakerBundle\Str; | ||||||
use Symfony\Bundle\MakerBundle\Util\DecoratorInfo; | ||||||
use Symfony\Bundle\MakerBundle\Validator; | ||||||
use Symfony\Component\Console\Command\Command; | ||||||
use Symfony\Component\Console\Input\InputArgument; | ||||||
use Symfony\Component\Console\Input\InputInterface; | ||||||
use Symfony\Component\Console\Input\InputOption; | ||||||
use Symfony\Component\Console\Question\ConfirmationQuestion; | ||||||
use Symfony\Component\Console\Question\Question; | ||||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | ||||||
|
||||||
/** | ||||||
* @author Benjamin Georgeault <[email protected]> | ||||||
*/ | ||||||
final class MakeDecorator extends AbstractMaker | ||||||
{ | ||||||
public function __construct( | ||||||
private readonly DecoratorHelper $helper, | ||||||
) { | ||||||
} | ||||||
|
||||||
public static function getCommandName(): string | ||||||
{ | ||||||
return 'make:decorator'; | ||||||
} | ||||||
|
||||||
public static function getCommandDescription(): string | ||||||
{ | ||||||
return 'Create a decorator of a service'; | ||||||
} | ||||||
|
||||||
public function configureCommand(Command $command, InputConfiguration $inputConfig): void | ||||||
{ | ||||||
$command | ||||||
->addArgument('id', InputArgument::OPTIONAL, 'The ID of the service to decorate.') | ||||||
->addArgument('decorator-class', InputArgument::OPTIONAL, \sprintf('The class name of the service to create (e.g. <fg=yellow>%sDecorator</>)', Str::asClassName(Str::getRandomTerm()))) | ||||||
->addOption('priority', null, InputOption::VALUE_REQUIRED, 'The priority of this decoration when multiple decorators are declared for the same service.') | ||||||
->addOption('on-invalid', null, InputOption::VALUE_REQUIRED, 'The behavior to adopt when the decoration is invalid.') | ||||||
->setHelp($this->getHelpFileContents('MakeDecorator.txt')) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||||
; | ||||||
|
||||||
$inputConfig->setArgumentAsNonInteractive('id'); | ||||||
$inputConfig->setArgumentAsNonInteractive('decorator-class'); | ||||||
} | ||||||
|
||||||
public function configureDependencies(DependencyBuilder $dependencies): void | ||||||
{ | ||||||
$dependencies->addClassDependency( | ||||||
AsDecorator::class, | ||||||
'dependency-injection', | ||||||
); | ||||||
} | ||||||
|
||||||
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void | ||||||
{ | ||||||
// Ask for service id. | ||||||
if (null === $input->getArgument('id')) { | ||||||
$argument = $command->getDefinition()->getArgument('id'); | ||||||
|
||||||
($question = new Question($argument->getDescription())) | ||||||
->setAutocompleterValues($suggestIds = $this->helper->suggestIds()) | ||||||
->setValidator(fn ($answer) => Validator::serviceExists($answer, $suggestIds)) | ||||||
->setMaxAttempts(3); | ||||||
|
||||||
$input->setArgument('id', $io->askQuestion($question)); | ||||||
} | ||||||
|
||||||
$id = $input->getArgument('id'); | ||||||
if (null === $realId = $this->helper->getRealId($id)) { | ||||||
$guessCount = \count($guessRealIds = $this->helper->guessRealIds($id)); | ||||||
|
||||||
if (0 === $guessCount) { | ||||||
throw new RuntimeCommandException(\sprintf('Cannot find nor guess service for given id "%s".', $id)); | ||||||
} elseif (1 === $guessCount) { | ||||||
$question = new ConfirmationQuestion(\sprintf('<fg=green>Did you mean</> <fg=yellow>"%s"</> <fg=green>?</>', $guessRealIds[0]), true); | ||||||
|
||||||
if (!$io->askQuestion($question)) { | ||||||
throw new RuntimeCommandException(\sprintf('Cannot find nor guess service for given id "%s".', $id)); | ||||||
} | ||||||
|
||||||
$input->setArgument('id', $id = $guessRealIds[0]); | ||||||
} else { | ||||||
$input->setArgument( | ||||||
'id', | ||||||
$id = $io->choice(\sprintf('Multiple services found for "%s", choice which one you want to decorate?', $id), $guessRealIds), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
); | ||||||
} | ||||||
} else { | ||||||
$input->setArgument('id', $id = $realId); | ||||||
} | ||||||
|
||||||
// Ask for decorator classname. | ||||||
if (null === $input->getArgument('decorator-class')) { | ||||||
$argument = $command->getDefinition()->getArgument('decorator-class'); | ||||||
|
||||||
$basename = Str::getShortClassName(match (true) { | ||||||
interface_exists($id) => Str::removeSuffix($id, 'Interface'), | ||||||
class_exists($id) => $id, | ||||||
default => Str::asClassName($id), | ||||||
}); | ||||||
|
||||||
$defaultClass = Str::asClassName(\sprintf('%s Decorator', $basename)); | ||||||
|
||||||
($question = new Question($argument->getDescription(), $defaultClass)) | ||||||
->setValidator(fn ($answer) => Validator::validateClassName(Validator::classDoesNotExist($answer))) | ||||||
->setMaxAttempts(3); | ||||||
|
||||||
$input->setArgument('decorator-class', $io->askQuestion($question)); | ||||||
} | ||||||
} | ||||||
|
||||||
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void | ||||||
{ | ||||||
$id = $input->getArgument('id'); | ||||||
|
||||||
$classNameDetails = $generator->createClassNameDetails( | ||||||
Validator::validateClassName(Validator::classDoesNotExist($input->getArgument('decorator-class'))), | ||||||
'', | ||||||
); | ||||||
|
||||||
$priority = $input->getOption('priority'); | ||||||
$onInvalid = $input->getOption('on-invalid'); | ||||||
|
||||||
$decoratedInfo = new DecoratorInfo( | ||||||
$classNameDetails->getFullName(), | ||||||
$id, | ||||||
$this->helper->getClass($id), | ||||||
empty($priority) ? null : $priority, | ||||||
null === $onInvalid || 1 === $onInvalid ? null : $onInvalid, | ||||||
); | ||||||
|
||||||
$classData = $decoratedInfo->getClassData(); | ||||||
|
||||||
$generator->generateClassFromClassData( | ||||||
$classData, | ||||||
'decorator/Decorator.tpl.php', | ||||||
[ | ||||||
'decorated_info' => $decoratedInfo, | ||||||
], | ||||||
); | ||||||
|
||||||
$generator->writeChanges(); | ||||||
|
||||||
$this->writeSuccessMessage($io); | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it works without but I d'ont find it very readable to not explictly create the array. Maybe it is acceptable in the maker bundle.