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
*/