-
-
Notifications
You must be signed in to change notification settings - Fork 407
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:*] interactivly install composer dependencies #1466
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,118 @@ | ||
<?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\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<[email protected]> | ||
* | ||
* @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 <fg=yellow>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)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?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\Dependency\Model; | ||
|
||
/** | ||
* @author Jesse Rushlow <[email protected]> | ||
* | ||
* @internal | ||
*/ | ||
abstract class AbstractClassDependency | ||
{ | ||
public function __construct( | ||
public string $className, | ||
public string $composerPackage, | ||
public bool $installAsRequireDev = false, | ||
public ?string $preInstallMessage = null, | ||
) { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?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\Dependency\Model; | ||
|
||
/** | ||
* @author Jesse Rushlow <[email protected]> | ||
* | ||
* @internal | ||
*/ | ||
final class OptionalClassDependency extends AbstractClassDependency | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?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\Dependency\Model; | ||
|
||
/** | ||
* @author Jesse Rushlow <[email protected]> | ||
* | ||
* @internal | ||
*/ | ||
final class RequiredClassDependency extends AbstractClassDependency | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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... | ||
} | ||
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. Not a fan of touching the abstraction - but it keeps BC in check w/ the interface change. (see note on that below) |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,20 +51,44 @@ public function configureCommand(Command $command, InputConfiguration $inputConf | |
; | ||
} | ||
|
||
public function configureDependencies(DependencyBuilder $dependencies): void | ||
public function configureComposerDependencies(DependencyManager $dependencyManager): void | ||
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. I'm wondering if we can simplify. A few things:
The tl;dr is A) Can we keep using if ($input->getOption('live')) {
$this->dependencyDownloader->installPackage('symfony/ux-live-component', $io);
} This could interactively ask them if they want to install and do it. Else throw an exception to exit the command. Or maybe we go further and just |
||
{ | ||
$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(); | ||
Comment on lines
+83
to
+84
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. Not sure how to go about using the manager in other maker methods (aside from configureDepends...) Gonna play around with this a bit more. but other ideas are welcome.. Thought about some sort of setter in |
||
} | ||
|
||
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void | ||
{ | ||
$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 <fg=yellow><twig:{$shortName} /></>."); | ||
$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)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,13 +11,15 @@ | |
|
||
namespace Symfony\Bundle\MakerBundle; | ||
|
||
use Symfony\Bundle\MakerBundle\Dependency\DependencyManager; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
|
||
/** | ||
* Interface that all maker commands must implement. | ||
* | ||
* @method static string getCommandDescription() | ||
* @method void configureComposerDependencies(DependencyManager $dependencyManager) | ||
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. Not a fan of touching the interface... better ideas are welcome.. |
||
* | ||
* @author Ryan Weaver <[email protected]> | ||
*/ | ||
|
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.
Originally went with a single class that had
bool $isRequired
to determine if the dependency was required or optional. I went with an abstract base model and aRequiredClassDependency
&&OptionalClassDependency
for better DX / code review readability... Atleast that was the intent..