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 new file mode 100644 index 000000000..fd690427f --- /dev/null +++ b/src/Dependency/DependencyManager.php @@ -0,0 +1,118 @@ + + * + * 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 = []; + + public function __construct( + private ConsoleStyle $io, + private bool $interactiveMode = true, + ) { + } + + public function addDependency(RequiredClassDependency|OptionalClassDependency|array $dependency): self + { + $dependencies = []; + + if (!\is_array($dependency)) { + $dependencies[] = $dependency; + } + + foreach ($dependencies as $dependency) { + if ($dependency instanceof RequiredClassDependency) { + $this->requiredClassDependencies[] = $dependency; + + continue; + } + + $this->optionalClassDependencies[] = $dependency; + } + + return $this; + } + + public function installRequiredDependencies(): self + { + 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(): self + { + 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( + 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..f5881c3a6 --- /dev/null +++ b/src/Dependency/Model/AbstractClassDependency.php @@ -0,0 +1,28 @@ + + * + * 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, + public ?string $preInstallMessage = null, + ) { + } +} 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 +{ +} 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 669d92b51..1b385dc1e 100644 --- a/src/Maker/MakeTwigComponent.php +++ b/src/Maker/MakeTwigComponent.php @@ -12,7 +12,9 @@ namespace Symfony\Bundle\MakerBundle\Maker; use Symfony\Bundle\MakerBundle\ConsoleStyle; -use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; +use Symfony\Bundle\MakerBundle\Dependency\Model\OptionalClassDependency; +use Symfony\Bundle\MakerBundle\Dependency\Model\RequiredClassDependency; use Symfony\Bundle\MakerBundle\Generator; use Symfony\Bundle\MakerBundle\InputConfiguration; use Symfony\Bundle\MakerBundle\Str; @@ -28,6 +30,8 @@ */ final class MakeTwigComponent extends AbstractMaker { + private DependencyManager $dependencyManager; + public static function getCommandName(): string { return 'make:twig-component'; @@ -47,9 +51,37 @@ public function configureCommand(Command $command, InputConfiguration $inputConf ; } - public function configureDependencies(DependencyBuilder $dependencies): void + public function configureComposerDependencies(DependencyManager $dependencyManager): void { - $dependencies->addClassDependency(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.' + )) + ->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 + { + if (!$input->getOption('live')) { + $input->setOption('live', $io->confirm('Make this a live component?', false)); + } + + if (!$input->getOption('live')) { + return; + } + + // @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 @@ -57,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', @@ -87,11 +115,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)); - } - } } 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 */