Skip to content

Commit

Permalink
[make:decorator] Add new maker to create decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
WedgeSama committed Nov 10, 2024
1 parent ce60831 commit dbbc516
Show file tree
Hide file tree
Showing 39 changed files with 1,733 additions and 9 deletions.
10 changes: 10 additions & 0 deletions config/help/MakeDecorator.txt
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.
6 changes: 6 additions & 0 deletions config/makers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
<tag name="maker.command" />
</service>

<service id="maker.maker.make_decorator" class="Symfony\Bundle\MakerBundle\Maker\MakeDecorator">
<argument /> <!-- Service locator of all existing services -->
<argument /> <!-- Array of services' ids -->
<tag name="maker.command" />
</service>

<service id="maker.maker.make_docker_database" class="Symfony\Bundle\MakerBundle\Maker\MakeDockerDatabase">
<argument type="service" id="maker.file_manager" />
<tag name="maker.command" />
Expand Down
34 changes: 34 additions & 0 deletions src/DependencyInjection/CompilerPass/MakeDecoratorPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* @author Benjamin Georgeault <[email protected]>
*/
class MakeDecoratorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('maker.maker.make_decorator')) {
return;
}

$container->getDefinition('maker.maker.make_decorator')
->replaceArgument(0, ServiceLocatorTagPass::register($container, $ids = $container->getServiceIds()))
->replaceArgument(1, $ids)
;
}
}
141 changes: 141 additions & 0 deletions src/Maker/MakeDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?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 Psr\Container\ContainerInterface;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
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\Question\Question;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;

/**
* @author Benjamin Georgeault <[email protected]>
*/
final class MakeDecorator extends AbstractMaker
{
/**
* @param array<string> $ids
*/
public function __construct(
private readonly ContainerInterface $container,
private readonly array $ids,
) {
}

public static function getCommandName(): string
{
return 'make:decorator';
}

public static function getCommandDescription(): string
{
return 'Create CRUD for Doctrine entity class';
}

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())))
->setHelp($this->getHelpFileContents('MakeDecorator.txt'))
;
}

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($this->ids)
->setValidator(fn ($answer) => Validator::serviceExists($answer, $this->ids))
->setMaxAttempts(3);

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

$id = $input->getArgument('id');

// 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'))),
'',
);

$decoratedInfo = $this->createDecoratorInfo($id, $classNameDetails->getFullName());
$classData = $decoratedInfo->getClassData();

$generator->generateClassFromClassData(
$classData,
'decorator/Decorator.tpl.php',
[
'decorated_info' => $decoratedInfo,
],
);

$generator->writeChanges();

$this->writeSuccessMessage($io);
}

private function createDecoratorInfo(string $id, string $decoratorClass): DecoratorInfo
{
return new DecoratorInfo(
$decoratorClass,
match (true) {
class_exists($id), interface_exists($id) => $id,
default => $this->container->get($id)::class,
},
$id,
);
}
}
2 changes: 2 additions & 0 deletions src/MakerBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Bundle\MakerBundle;

use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeDecoratorPass;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\RemoveMissingParametersPass;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\SetDoctrineAnnotatedPrefixesPass;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
Expand Down Expand Up @@ -69,6 +70,7 @@ public function build(ContainerBuilder $container): void
{
// add a priority so we run before the core command pass
$container->addCompilerPass(new MakeCommandRegistrationPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
$container->addCompilerPass(new MakeDecoratorPass());
$container->addCompilerPass(new RemoveMissingParametersPass());
$container->addCompilerPass(new SetDoctrineAnnotatedPrefixesPass());
}
Expand Down
44 changes: 36 additions & 8 deletions src/Util/ClassSource/Model/ClassData.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,44 @@ private function __construct(
private bool $isFinal = true,
private string $rootNamespace = 'App',
private ?string $classSuffix = null,
public readonly ?array $implements = null,
) {
if (str_starts_with(haystack: $this->namespace, needle: $this->rootNamespace)) {
$this->namespace = substr_replace(string: $this->namespace, replace: '', offset: 0, length: \strlen($this->rootNamespace) + 1);
}
}

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

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

$useStatements = new UseStatementGenerator($useStatements);
$className = Str::asClassName($className);

$useStatements = new UseStatementGenerator($useStatements, [$className]);

if ($extendsClass) {
$useStatements->addUseStatement($extendsClass);
$useStatements->addUseStatement($extendsClass, 'Base');
}

if ($implements) {
array_walk($implements, function (string &$interface) use ($useStatements) {
$useStatements->addUseStatement($interface, 'Base');
$interface = $useStatements->getShortName($interface);
});
}

return new self(
className: Str::asClassName($className),
className: $className,
namespace: Str::getNamespace($class),
extends: null === $extendsClass ? null : Str::getShortClassName($extendsClass),
extends: null === $extendsClass ? null : $useStatements->getShortName($extendsClass),
isEntity: $isEntity,
useStatementGenerator: $useStatements,
classSuffix: $suffix,
implements: $implements,
);
}

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

return \sprintf('%sclass %s%s',
$implementsDeclaration = '';

if (null !== $this->implements) {
$implementsDeclaration = \sprintf(' implements %s', implode(', ', $this->implements));
}

return \sprintf('%sclass %s%s%s',
$this->isFinal ? 'final ' : '',
$this->className,
$extendsDeclaration,
$implementsDeclaration,
);
}

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

public function addUseStatement(array|string $useStatement): self
public function addUseStatement(array|string $useStatement, ?string $aliasPrefixIfExist = null): self
{
$this->useStatementGenerator->addUseStatement($useStatement);
$this->useStatementGenerator->addUseStatement($useStatement, $aliasPrefixIfExist);

return $this;
}
Expand All @@ -155,4 +173,14 @@ public function getUseStatements(): string
{
return (string) $this->useStatementGenerator;
}

public function getUseStatementShortName(string $className): string
{
return $this->useStatementGenerator->getShortName($className);
}

public function hasUseStatement(string $className): bool
{
return $this->useStatementGenerator->hasUseStatement($className);
}
}
61 changes: 61 additions & 0 deletions src/Util/ClassSource/Model/ClassMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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\Util\ClassSource\Model;

/**
* @author Benjamin Georgeault <[email protected]>
*
* @internal
*/
final class ClassMethod
{
/**
* @param MethodArgument[] $arguments
*/
public function __construct(
private readonly string $name,
private readonly array $arguments = [],
private readonly ?string $returnType = null,
private readonly bool $isStatic = false,
) {
}

public function getName(): string
{
return $this->name;
}

public function isReturnVoid(): bool
{
return 'void' === $this->returnType;
}

public function isStatic(): bool
{
return $this->isStatic;
}

public function getDeclaration(): string
{
return \sprintf('public %sfunction %s(%s)%s',
$this->isStatic ? 'static ' : '',
$this->name,
implode(', ', array_map(fn (MethodArgument $arg) => $arg->getDeclaration(), $this->arguments)),
$this->returnType ? ': '.$this->returnType : '',
);
}

public function getArgumentsUse(): string
{
return implode(', ', array_map(fn (MethodArgument $arg) => $arg->getVariable(), $this->arguments));
}
}
Loading

0 comments on commit dbbc516

Please sign in to comment.