From ad7b19ac4e610432a3ea476a5d772212b7a8daf9 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Wed, 28 Feb 2024 01:33:48 -0500 Subject: [PATCH 1/4] [make:*] interactively install dependencies --- src/Dependency/DependencyManager.php | 106 ++++++++++++++++++ .../Model/AbstractClassDependency.php | 27 +++++ .../Model/OptionalClassDependency.php | 21 ++++ .../Model/RequiredClassDependency.php | 21 ++++ 4 files changed, 175 insertions(+) create mode 100644 src/Dependency/DependencyManager.php create mode 100644 src/Dependency/Model/AbstractClassDependency.php create mode 100644 src/Dependency/Model/OptionalClassDependency.php create mode 100644 src/Dependency/Model/RequiredClassDependency.php diff --git a/src/Dependency/DependencyManager.php b/src/Dependency/DependencyManager.php new file mode 100644 index 000000000..3eabdcdc0 --- /dev/null +++ b/src/Dependency/DependencyManager.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Dependency; + +use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\Dependency\Model\OptionalClassDependency; +use Symfony\Bundle\MakerBundle\Dependency\Model\RequiredClassDependency; +use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Process\Process; + +/** + * @author Jesse Rushlow + * + * @internal + */ +final class DependencyManager +{ + /** @var RequiredClassDependency[] */ + private array $requiredClassDependencies = []; + + /** @var OptionalClassDependency[] */ + private array $optionalClassDependencies = []; + + private ConsoleStyle $io; + + public function addRequiredDependency(RequiredClassDependency $dependency): self + { + $this->requiredClassDependencies[] = $dependency; + + return $this; + } + + public function addOptionalDependency(OptionalClassDependency $dependency): self + { + $this->optionalClassDependencies[] = $dependency; + + return $this; + } + + public function installRequiredDependencies(ConsoleStyle $io, ?string $preInstallMessage): self + { + $this->io = $io; + + $preInstallMessage ?: $this->io->caution($preInstallMessage); + + foreach ($this->requiredClassDependencies as $dependency) { + if (class_exists($dependency->className) || !$this->askToInstallDependency($dependency)) { + continue; + } + + $this->runComposer($dependency); + } + + return $this; + } + + public function installOptionalDependencies(ConsoleStyle $io, ?string $preInstallMessage): self + { + $this->io = $io; + + $preInstallMessage ?: $this->io->caution($preInstallMessage); + + foreach ($this->optionalClassDependencies as $dependency) { + if (class_exists($dependency->className) || !$this->askToInstallDependency($dependency)) { + continue; + } + + $this->runComposer($dependency); + } + + return $this; + } + + private function askToInstallDependency(RequiredClassDependency|OptionalClassDependency $dependency): bool + { + return $this->io->confirm( + question: sprintf('Do you want us to run composer require %s for you?', $dependency->composerPackage), + default: true // @TODO - Should we default to yes or no on this... + ); + } + + private function runComposer(RequiredClassDependency|OptionalClassDependency $dependency): void + { + $process = Process::fromShellCommandline( + sprintf('composer require%s %s', $dependency->installAsRequireDev ?: ' --dev', $dependency->composerPackage) + ); + + if (Command::SUCCESS === $process->run()) { + return; + } + + $this->io->block($process->getErrorOutput()); + + throw new RuntimeCommandException(sprintf('Oops! There was a problem installing "%s". You\'ll need to install the package manually.', $dependency->className)); + } +} diff --git a/src/Dependency/Model/AbstractClassDependency.php b/src/Dependency/Model/AbstractClassDependency.php new file mode 100644 index 000000000..6df76df2f --- /dev/null +++ b/src/Dependency/Model/AbstractClassDependency.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Dependency\Model; + +/** + * @author Jesse Rushlow + * + * @internal + */ +abstract class AbstractClassDependency +{ + public function __construct( + public string $className, + public string $composerPackage, + public bool $installAsRequireDev = false, + ) { + } +} diff --git a/src/Dependency/Model/OptionalClassDependency.php b/src/Dependency/Model/OptionalClassDependency.php new file mode 100644 index 000000000..c557ef78a --- /dev/null +++ b/src/Dependency/Model/OptionalClassDependency.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Dependency\Model; + +/** + * @author Jesse Rushlow + * + * @internal + */ +final class OptionalClassDependency extends AbstractClassDependency +{ +} diff --git a/src/Dependency/Model/RequiredClassDependency.php b/src/Dependency/Model/RequiredClassDependency.php new file mode 100644 index 000000000..d2ffd215e --- /dev/null +++ b/src/Dependency/Model/RequiredClassDependency.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Dependency\Model; + +/** + * @author Jesse Rushlow + * + * @internal + */ +final class RequiredClassDependency extends AbstractClassDependency +{ +} From 01ca14c3ff4f6c91fed75d35bee67501b765684d Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Wed, 28 Feb 2024 01:34:41 -0500 Subject: [PATCH 2/4] [make:twig-component] --- src/Maker/MakeTwigComponent.php | 47 +++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Maker/MakeTwigComponent.php b/src/Maker/MakeTwigComponent.php index 669d92b51..f388e0265 100644 --- a/src/Maker/MakeTwigComponent.php +++ b/src/Maker/MakeTwigComponent.php @@ -12,6 +12,9 @@ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; +use Symfony\Bundle\MakerBundle\Dependency\Model\OptionalClassDependency; +use Symfony\Bundle\MakerBundle\Dependency\Model\RequiredClassDependency; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; @@ -28,6 +31,11 @@ */ final class MakeTwigComponent extends AbstractMaker { + public function __construct( + private DependencyManager $dependencyManager = new DependencyManager(), + ) { + } + public static function getCommandName(): string { return 'make:twig-component'; @@ -49,7 +57,37 @@ public function configureCommand(Command $command, InputConfiguration $inputConf public function configureDependencies(DependencyBuilder $dependencies): void { - $dependencies->addClassDependency(AsTwigComponent::class, 'symfony/ux-twig-component'); + $this->dependencyManager + ->addRequiredDependency(new RequiredClassDependency( + AsTwigComponent::class, + 'symfony/ux-twig-component', + )) + ->addOptionalDependency(new OptionalClassDependency( + AsLiveComponent::class, + 'symfony/ux-live-component', + )) + ; + } + + public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void + { + $this->dependencyManager->installRequiredDependencies( + io: $io, + preInstallMessage: 'This command requires the Symfony UX Twig Component Package.' + ); + + if (!$input->getOption('live')) { + $input->setOption('live', $io->confirm('Make this a live component?', false)); + } + + if (!$input->getOption('live')) { + return; + } + + $this->dependencyManager->installOptionalDependencies( + io: $io, + preInstallMessage: 'The Symfony UX Live Component is needed to make this a live component.' + ); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void @@ -87,11 +125,4 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $io->writeln(" To render the component, use ."); $io->newLine(); } - - public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void - { - if (!$input->getOption('live')) { - $input->setOption('live', $io->confirm('Make this a live component?', false)); - } - } } From 169053a60a32a04c60102e185dd9950ab4a34e93 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Wed, 28 Feb 2024 02:40:12 -0500 Subject: [PATCH 3/4] [make:security:form-login] --- phpunit.xml.dist | 1 + src/Command/MakerCommand.php | 8 ++++ src/Dependency/DependencyManager.php | 48 ++++++++++++------- .../Model/AbstractClassDependency.php | 1 + src/Maker/AbstractMaker.php | 12 +++++ src/Maker/MakeTwigComponent.php | 38 +++++++-------- src/Maker/Security/MakeFormLogin.php | 26 ++++------ src/MakerInterface.php | 2 + 8 files changed, 80 insertions(+), 56 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d35e0fca4..f75cfc7d0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,6 +19,7 @@ + diff --git a/src/Command/MakerCommand.php b/src/Command/MakerCommand.php index 4188d2188..cb313bde0 100644 --- a/src/Command/MakerCommand.php +++ b/src/Command/MakerCommand.php @@ -13,6 +13,7 @@ use Symfony\Bundle\MakerBundle\ApplicationAwareMakerInterface; use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; @@ -62,6 +63,13 @@ protected function initialize(InputInterface $input, OutputInterface $output): v $dependencies = new DependencyBuilder(); $this->maker->configureDependencies($dependencies, $input); + // env is for tests - this way we don't have to add a `y` to every test when a dependency is needed. + $dependencyManager = new DependencyManager($this->io, getenv('MAKER_INTERACTIVE_DEPENDS') ?? true); + + $this->maker->configureComposerDependencies($dependencyManager); + + $dependencyManager->installRequiredDependencies(); + if ($missingPackagesMessage = $dependencies->getMissingPackagesMessage($this->getName())) { throw new RuntimeCommandException($missingPackagesMessage); } diff --git a/src/Dependency/DependencyManager.php b/src/Dependency/DependencyManager.php index 3eabdcdc0..fd690427f 100644 --- a/src/Dependency/DependencyManager.php +++ b/src/Dependency/DependencyManager.php @@ -31,56 +31,68 @@ final class DependencyManager /** @var OptionalClassDependency[] */ private array $optionalClassDependencies = []; - private ConsoleStyle $io; + public function __construct( + private ConsoleStyle $io, + private bool $interactiveMode = true, + ) { + } - public function addRequiredDependency(RequiredClassDependency $dependency): self + public function addDependency(RequiredClassDependency|OptionalClassDependency|array $dependency): self { - $this->requiredClassDependencies[] = $dependency; + $dependencies = []; - return $this; - } + if (!\is_array($dependency)) { + $dependencies[] = $dependency; + } - public function addOptionalDependency(OptionalClassDependency $dependency): self - { - $this->optionalClassDependencies[] = $dependency; + foreach ($dependencies as $dependency) { + if ($dependency instanceof RequiredClassDependency) { + $this->requiredClassDependencies[] = $dependency; + + continue; + } + + $this->optionalClassDependencies[] = $dependency; + } return $this; } - public function installRequiredDependencies(ConsoleStyle $io, ?string $preInstallMessage): self + public function installRequiredDependencies(): self { - $this->io = $io; - - $preInstallMessage ?: $this->io->caution($preInstallMessage); - foreach ($this->requiredClassDependencies as $dependency) { if (class_exists($dependency->className) || !$this->askToInstallDependency($dependency)) { continue; } + $dependency->preInstallMessage ?: $this->io->caution($dependency->preInstallMessage); + $this->runComposer($dependency); } return $this; } - public function installOptionalDependencies(ConsoleStyle $io, ?string $preInstallMessage): self + public function installOptionalDependencies(): self { - $this->io = $io; - - $preInstallMessage ?: $this->io->caution($preInstallMessage); - foreach ($this->optionalClassDependencies as $dependency) { if (class_exists($dependency->className) || !$this->askToInstallDependency($dependency)) { continue; } + $dependency->preInstallMessage ?: $this->io->caution($dependency->preInstallMessage); + $this->runComposer($dependency); } return $this; } + public function installInteractively(): bool + { + return $this->interactiveMode; + } + private function askToInstallDependency(RequiredClassDependency|OptionalClassDependency $dependency): bool { return $this->io->confirm( diff --git a/src/Dependency/Model/AbstractClassDependency.php b/src/Dependency/Model/AbstractClassDependency.php index 6df76df2f..f5881c3a6 100644 --- a/src/Dependency/Model/AbstractClassDependency.php +++ b/src/Dependency/Model/AbstractClassDependency.php @@ -22,6 +22,7 @@ public function __construct( public string $className, public string $composerPackage, public bool $installAsRequireDev = false, + public ?string $preInstallMessage = null, ) { } } diff --git a/src/Maker/AbstractMaker.php b/src/Maker/AbstractMaker.php index 8341fdd6f..ded5b9d64 100644 --- a/src/Maker/AbstractMaker.php +++ b/src/Maker/AbstractMaker.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Component\Console\Command\Command; @@ -54,4 +55,15 @@ protected function addDependencies(array $dependencies, ?string $message = null) $message ); } + + public function configureComposerDependencies(DependencyManager $dependencyManager): void + { + // @TODO - method here in abstract prevents BC with signature added to `MakerInterface::class` + } + + public function configureDependencies(DependencyBuilder $dependencies) + { + // @TODO - do we deprecate this method in favor of the one above. then remove in 2.x + // @TODO - still have plenty of work todo to determine if thats possible or a good idea... + } } diff --git a/src/Maker/MakeTwigComponent.php b/src/Maker/MakeTwigComponent.php index f388e0265..698c6927f 100644 --- a/src/Maker/MakeTwigComponent.php +++ b/src/Maker/MakeTwigComponent.php @@ -15,7 +15,6 @@ use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; use Symfony\Bundle\MakerBundle\Dependency\Model\OptionalClassDependency; use Symfony\Bundle\MakerBundle\Dependency\Model\RequiredClassDependency; -use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; @@ -31,10 +30,7 @@ */ final class MakeTwigComponent extends AbstractMaker { - public function __construct( - private DependencyManager $dependencyManager = new DependencyManager(), - ) { - } + private DependencyManager $dependencyManager; public static function getCommandName(): string { @@ -55,27 +51,27 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ; } - public function configureDependencies(DependencyBuilder $dependencies): void + public function configureComposerDependencies(DependencyManager $dependencyManager): void { - $this->dependencyManager - ->addRequiredDependency(new RequiredClassDependency( - AsTwigComponent::class, - 'symfony/ux-twig-component', + // $this is a hack - we need the manager later in `interact()` + $this->dependencyManager = $dependencyManager; + + $dependencyManager + ->addDependency(new RequiredClassDependency( + className: AsTwigComponent::class, + composerPackage: 'symfony/ux-twig-component', + preInstallMessage: 'This command requires the Symfony UX Twig Component Package.' )) - ->addOptionalDependency(new OptionalClassDependency( - AsLiveComponent::class, - 'symfony/ux-live-component', + ->addDependency(new OptionalClassDependency( + className: AsLiveComponent::class, + composerPackage: 'symfony/ux-live-component', + preInstallMessage: 'The Symfony UX Live Component is needed to make this a live component.' )) ; } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void { - $this->dependencyManager->installRequiredDependencies( - io: $io, - preInstallMessage: 'This command requires the Symfony UX Twig Component Package.' - ); - if (!$input->getOption('live')) { $input->setOption('live', $io->confirm('Make this a live component?', false)); } @@ -84,10 +80,8 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma return; } - $this->dependencyManager->installOptionalDependencies( - io: $io, - preInstallMessage: 'The Symfony UX Live Component is needed to make this a live component.' - ); + // @TODO - with the dependencyManager in `Command` -> we can't use it outside of configure dependencies..... + $this->dependencyManager->installOptionalDependencies(); } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void diff --git a/src/Maker/Security/MakeFormLogin.php b/src/Maker/Security/MakeFormLogin.php index 34d5bd12f..d7249b6b0 100644 --- a/src/Maker/Security/MakeFormLogin.php +++ b/src/Maker/Security/MakeFormLogin.php @@ -14,7 +14,8 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\MakerBundle\ConsoleStyle; -use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; +use Symfony\Bundle\MakerBundle\Dependency\Model\RequiredClassDependency; use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Bundle\MakerBundle\FileManager; use Symfony\Bundle\MakerBundle\Generator; @@ -77,22 +78,15 @@ public static function getCommandDescription(): string return 'Generate the code needed for the form_login authenticator'; } - public function configureDependencies(DependencyBuilder $dependencies): void + public function configureComposerDependencies(DependencyManager $dependencyManager): void { - $dependencies->addClassDependency( - SecurityBundle::class, - 'security' - ); - - $dependencies->addClassDependency(TwigBundle::class, 'twig'); - - // needed to update the YAML files - $dependencies->addClassDependency( - Yaml::class, - 'yaml' - ); - - $dependencies->addClassDependency(DoctrineBundle::class, 'orm'); + $dependencyManager + ->addDependency([ + new RequiredClassDependency(SecurityBundle::class, 'security'), + new RequiredClassDependency(TwigBundle::class, 'twig'), + new RequiredClassDependency(Yaml::class, 'yaml'), + new RequiredClassDependency(DoctrineBundle::class, 'orm'), + ]); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void diff --git a/src/MakerInterface.php b/src/MakerInterface.php index 52f70f205..280f62b45 100644 --- a/src/MakerInterface.php +++ b/src/MakerInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\MakerBundle; +use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -18,6 +19,7 @@ * Interface that all maker commands must implement. * * @method static string getCommandDescription() + * @method void configureComposerDependencies(DependencyManager $dependencyManager) * * @author Ryan Weaver */ From 0b215ad34ddf8e49e85e924758552e6bfdddd760 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Wed, 28 Feb 2024 14:09:17 -0500 Subject: [PATCH 4/4] dont need that anymore... --- src/Maker/MakeTwigComponent.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Maker/MakeTwigComponent.php b/src/Maker/MakeTwigComponent.php index 698c6927f..1b385dc1e 100644 --- a/src/Maker/MakeTwigComponent.php +++ b/src/Maker/MakeTwigComponent.php @@ -89,10 +89,6 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $name = $input->getArgument('name'); $live = $input->getOption('live'); - if ($live && !class_exists(AsLiveComponent::class)) { - throw new \RuntimeException('You must install symfony/ux-live-component to create a live component (composer require symfony/ux-live-component)'); - } - $factory = $generator->createClassNameDetails( $name, 'Twig\\Components',