diff --git a/.vortex/installer/composer.lock b/.vortex/installer/composer.lock
index 6afd2b253..85cc18cdd 100644
--- a/.vortex/installer/composer.lock
+++ b/.vortex/installer/composer.lock
@@ -239,16 +239,16 @@
},
{
"name": "laravel/prompts",
- "version": "v0.3.5",
+ "version": "v0.3.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1"
+ "reference": "86a8b692e8661d0fb308cec64f3d176821323077"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1",
- "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077",
+ "reference": "86a8b692e8661d0fb308cec64f3d176821323077",
"shasum": ""
},
"require": {
@@ -292,9 +292,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.5"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.6"
},
- "time": "2025-02-11T13:34:40+00:00"
+ "time": "2025-07-07T14:17:42+00:00"
},
{
"name": "psr/container",
@@ -2325,16 +2325,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.13.1",
+ "version": "1.13.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
+ "reference": "faed855a7b5f4d4637717c2b3863e277116beb36"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
- "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36",
+ "reference": "faed855a7b5f4d4637717c2b3863e277116beb36",
"shasum": ""
},
"require": {
@@ -2373,7 +2373,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3"
},
"funding": [
{
@@ -2381,7 +2381,7 @@
"type": "tidelift"
}
],
- "time": "2025-04-29T12:36:36+00:00"
+ "time": "2025-07-05T12:25:42+00:00"
},
{
"name": "nikic/php-parser",
@@ -2561,16 +2561,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "2.1.0",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
+ "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
- "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8",
+ "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8",
"shasum": ""
},
"require": {
@@ -2602,9 +2602,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0"
},
- "time": "2025-02-19T13:28:12+00:00"
+ "time": "2025-07-13T07:04:09+00:00"
},
{
"name": "phpstan/phpstan",
@@ -3001,16 +3001,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.25",
+ "version": "11.5.27",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "864ab32b3ff52058f917c5b19b3cef821e4a4f1b"
+ "reference": "446d43867314781df7e9adf79c3ec7464956fd8f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/864ab32b3ff52058f917c5b19b3cef821e4a4f1b",
- "reference": "864ab32b3ff52058f917c5b19b3cef821e4a4f1b",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f",
+ "reference": "446d43867314781df7e9adf79c3ec7464956fd8f",
"shasum": ""
},
"require": {
@@ -3020,7 +3020,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.13.1",
+ "myclabs/deep-copy": "^1.13.3",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
@@ -3082,7 +3082,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.25"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.27"
},
"funding": [
{
@@ -3106,20 +3106,20 @@
"type": "tidelift"
}
],
- "time": "2025-06-27T04:36:07+00:00"
+ "time": "2025-07-11T04:10:06+00:00"
},
{
"name": "rector/rector",
- "version": "2.1.0",
+ "version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "d513dea45a94394b660e15c155d1fa27826f8e30"
+ "reference": "d0917c069bb0d9bb06ed111cf052510f609015a4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/d513dea45a94394b660e15c155d1fa27826f8e30",
- "reference": "d513dea45a94394b660e15c155d1fa27826f8e30",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/d0917c069bb0d9bb06ed111cf052510f609015a4",
+ "reference": "d0917c069bb0d9bb06ed111cf052510f609015a4",
"shasum": ""
},
"require": {
@@ -3158,7 +3158,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.1.0"
+ "source": "https://github.com/rectorphp/rector/tree/2.1.1"
},
"funding": [
{
@@ -3166,7 +3166,7 @@
"type": "github"
}
],
- "time": "2025-06-24T20:26:57+00:00"
+ "time": "2025-07-10T11:31:31+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -4098,16 +4098,16 @@
},
{
"name": "slevomat/coding-standard",
- "version": "8.19.0",
+ "version": "8.19.1",
"source": {
"type": "git",
"url": "https://github.com/slevomat/coding-standard.git",
- "reference": "9cc50509c3912d465a9f6898069f64cf2abf4e8a"
+ "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/9cc50509c3912d465a9f6898069f64cf2abf4e8a",
- "reference": "9cc50509c3912d465a9f6898069f64cf2abf4e8a",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220",
+ "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220",
"shasum": ""
},
"require": {
@@ -4147,7 +4147,7 @@
],
"support": {
"issues": "https://github.com/slevomat/coding-standard/issues",
- "source": "https://github.com/slevomat/coding-standard/tree/8.19.0"
+ "source": "https://github.com/slevomat/coding-standard/tree/8.19.1"
},
"funding": [
{
@@ -4159,20 +4159,20 @@
"type": "tidelift"
}
],
- "time": "2025-06-07T15:02:20+00:00"
+ "time": "2025-06-09T17:53:57+00:00"
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.13.0",
+ "version": "3.13.2",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "65ff2489553b83b4597e89c3b8b721487011d186"
+ "reference": "5b5e3821314f947dd040c70f7992a64eac89025c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186",
- "reference": "65ff2489553b83b4597e89c3b8b721487011d186",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c",
+ "reference": "5b5e3821314f947dd040c70f7992a64eac89025c",
"shasum": ""
},
"require": {
@@ -4243,7 +4243,7 @@
"type": "thanks_dev"
}
],
- "time": "2025-05-11T03:36:00+00:00"
+ "time": "2025-06-17T22:17:01+00:00"
},
{
"name": "staabm/side-effects-detector",
diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php
index fc7461951..41fc40412 100644
--- a/.vortex/installer/src/Command/InstallCommand.php
+++ b/.vortex/installer/src/Command/InstallCommand.php
@@ -102,7 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
static::header();
- $pm->prompt();
+ $pm->runPrompts();
Tui::list($pm->getResponsesSummary(), 'Installation summary');
@@ -116,36 +116,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int
Tui::action(
label: 'β¬οΈ Downloading Vortex',
- hint: fn(): string => sprintf('Downloading from "%s" repository at commit "%s"', ...Downloader::parseUri($this->config->get(Config::REPO))),
- success: 'Vortex downloaded',
action: function (): void {
$version = (new Downloader())->download($this->config->get(Config::REPO), $this->config->get(Config::REF), $this->config->get(Config::TMP));
$this->config->set(Config::VERSION, $version);
},
+ hint: fn(): string => sprintf('Downloading from "%s" repository at commit "%s"', ...Downloader::parseUri($this->config->get(Config::REPO))),
+ success: 'Vortex downloaded',
);
Tui::action(
label: 'βοΈ Customizing Vortex for your project',
+ action: fn() => $pm->runProcessors(),
success: 'Vortex was customized for your project',
- action: fn() => $pm->process(),
);
Tui::action(
label: 'π₯£ Preparing destination directory',
- success: 'Destination directory is ready',
action: fn(): array => $this->prepareDestination(),
+ success: 'Destination directory is ready',
);
Tui::action(
label: 'β‘οΈ Copying files to the destination directory',
- success: 'Files copied to destination directory',
action: fn() => $this->copyFiles(),
+ success: 'Files copied to destination directory',
);
Tui::action(
label: 'π¬ Preparing demo content',
- success: 'Demo content prepared',
action: fn(): string|array => $this->handleDemo(),
+ success: 'Demo content prepared',
);
// @todo Implement the demo mode.
diff --git a/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php b/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php
index c6a8c5882..77fd64dba 100644
--- a/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php
+++ b/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php
@@ -20,7 +20,7 @@ abstract class AbstractHandler implements HandlerInterface {
protected string $tmpDir;
/**
- * The webroot directory name.
+ * The webroot directory.
*/
protected string $webroot;
@@ -37,9 +37,11 @@ abstract class AbstractHandler implements HandlerInterface {
protected array $responses;
/**
- * The configuration object.
+ * Constructor.
*/
- public function __construct(protected Config $config) {
+ public function __construct(
+ protected Config $config,
+ ) {
$this->dstDir = $this->config->getDst();
$this->tmpDir = $this->config->get(Config::TMP);
}
@@ -47,53 +49,119 @@ public function __construct(protected Config $config) {
/**
* {@inheritdoc}
*/
- public function setWebroot(string $webroot): static {
- $this->webroot = $webroot;
+ public static function id(): string {
+ $reflector = new \ReflectionClass(static::class);
- return $this;
+ $filename = $reflector->getFileName();
+
+ if ($filename === FALSE) {
+ throw new \RuntimeException(sprintf('Could not determine the filename of the handler class %s.', static::class));
+ }
+
+ return Converter::machine(Converter::pascal2snake(str_replace('Handler', '', basename($filename, '.php'))));
}
/**
* {@inheritdoc}
*/
- public function setResponses(array $responses): static {
- $this->responses = $responses;
- $this->setWebroot($responses[Webroot::id()] ?? Webroot::WEB);
- // Set the response for current handler as a shorthand.
- // Some handlers may want to perform an action on the empty responses, so
- // it is up to the handler's processor to check for the presence of the
- // value in a set response.
- $this->response = $this->responses[static::id()] ?? NULL;
+ public static function description(array $responses): ?string {
+ return NULL;
+ }
- return $this;
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return NULL;
}
/**
* {@inheritdoc}
- *
- * Automatically generate the handler ID based on the class name.
*/
- public static function id(): string {
- $reflector = new \ReflectionClass(static::class);
+ public function placeholder(array $responses): ?string {
+ return NULL;
+ }
- $filename = $reflector->getFileName();
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return FALSE;
+ }
- if ($filename === FALSE) {
- throw new \RuntimeException(sprintf('Could not determine the filename of the handler class %s.', static::class));
- }
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return NULL;
+ }
- return Converter::machine(Converter::pascal2snake(str_replace('Handler', '', basename($filename, '.php'))));
+ /**
+ * {@inheritdoc}
+ */
+ public function shouldRun(array $responses): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolvedValue(array $responses): null|string|bool|array {
+ return NULL;
}
/**
* {@inheritdoc}
*/
- abstract public function discover(): null|string|bool|array;
+ public function resolvedMessage(array $responses): ?string {
+ return NULL;
+ }
/**
* {@inheritdoc}
*/
- abstract public function process(): void;
+ public function setResponses(array $responses): static {
+ $this->responses = $responses;
+ $this->setWebroot($responses[Webroot::id()] ?? Webroot::WEB);
+
+ // Set the response for the current handler as a shorthand.
+ // Some handlers may want to perform an action on the empty responses, so
+ // it is up to the handler's processor to check for the presence of the
+ // value in a set response.
+ $this->response = $this->responses[static::id()] ?? NULL;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setWebroot(string $webroot): static {
+ $this->webroot = $webroot;
+
+ return $this;
+ }
/**
* Check that Vortex is installed for this project.
diff --git a/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php b/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php
index a5ed1fb28..26306f94b 100644
--- a/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php
+++ b/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php
@@ -10,6 +10,37 @@ class AiCodeInstructions extends AbstractHandler {
const CLAUDE = 'claude';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π€ AI code assistant instructions';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Helps AI coding assistants to understand the project better.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::CLAUDE => 'Anthropic Claude',
+ self::NONE => 'None',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return self::NONE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php b/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php
index 9acabf4ad..c9ce23078 100644
--- a/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php
+++ b/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php
@@ -6,6 +6,27 @@
class AssignAuthorPr extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π€ Auto-assign the author to their PR?';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Helps to keep the PRs organized.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return TRUE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/CiProvider.php b/.vortex/installer/src/Prompts/Handlers/CiProvider.php
index a3618367e..d0b71652a 100644
--- a/.vortex/installer/src/Prompts/Handlers/CiProvider.php
+++ b/.vortex/installer/src/Prompts/Handlers/CiProvider.php
@@ -14,6 +14,44 @@ class CiProvider extends AbstractHandler {
const CIRCLECI = 'circleci';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Continuous Integration provider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Both providers support equivalent workflow.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ $options = [
+ self::NONE => 'None',
+ self::GITHUB_ACTIONS => 'GitHub Actions',
+ self::CIRCLECI => 'CircleCI',
+ ];
+
+ if (isset($responses[CodeProvider::id()]) && $responses[CodeProvider::id()] !== CodeProvider::GITHUB) {
+ unset($options[self::GITHUB_ACTIONS]);
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return self::GITHUB_ACTIONS;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/CodeProvider.php b/.vortex/installer/src/Prompts/Handlers/CodeProvider.php
index e254e354a..e161bfcc0 100644
--- a/.vortex/installer/src/Prompts/Handlers/CodeProvider.php
+++ b/.vortex/installer/src/Prompts/Handlers/CodeProvider.php
@@ -12,6 +12,37 @@ class CodeProvider extends AbstractHandler {
const OTHER = 'other';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'ποΈ Repository provider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Vortex offers full automation with GitHub, while support for other providers is limited.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::GITHUB => 'GitHub',
+ self::OTHER => 'Other',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return self::GITHUB;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php b/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php
index 30c041c9b..688acfe2b 100644
--- a/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php
+++ b/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php
@@ -21,6 +21,68 @@ class DatabaseDownloadSource extends AbstractHandler {
const NONE = 'none';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π‘ Database source';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'The database can be downloaded as an exported dump file or pre-packaged in a container image.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ $options = [
+ self::URL => 'π URL download',
+ self::FTP => 'π FTP download',
+ self::ACQUIA => 'π§ Acquia backup',
+ self::LAGOON => 'π Lagoon environment',
+ self::CONTAINER_REGISTRY => 'π³ Container registry',
+ self::NONE => 'π
None',
+ ];
+
+ if (isset($responses[HostingProvider::id()])) {
+ if ($responses[HostingProvider::id()] === HostingProvider::ACQUIA) {
+ unset($options[self::LAGOON]);
+ }
+
+ if ($responses[HostingProvider::id()] === HostingProvider::LAGOON) {
+ unset($options[self::ACQUIA]);
+ }
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function shouldRun(array $responses): bool {
+ return isset($responses[ProvisionType::id()]) && $responses[ProvisionType::id()] !== ProvisionType::PROFILE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[HostingProvider::id()])) {
+ return match ($responses[HostingProvider::id()]) {
+ HostingProvider::ACQUIA => self::ACQUIA,
+ HostingProvider::LAGOON => self::LAGOON,
+ default => self::URL,
+ };
+ }
+
+ return self::URL;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php b/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php
index 65a7b0972..5cab273e7 100644
--- a/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php
+++ b/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php
@@ -4,11 +4,70 @@
namespace DrevOps\VortexInstaller\Prompts\Handlers;
+use DrevOps\VortexInstaller\Utils\Converter;
use DrevOps\VortexInstaller\Utils\Env;
use DrevOps\VortexInstaller\Utils\File;
+use DrevOps\VortexInstaller\Utils\Validator;
class DatabaseImage extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π·οΈ What is your database container image name and a tag?';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Use "latest" tag for the latest version. CI will be building this image overnight.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ // Generate placeholder from OrgMachineName and MachineName if available.
+ if (isset($responses[OrgMachineName::id()]) && isset($responses[MachineName::id()])
+ && !empty($responses[OrgMachineName::id()]) && !empty($responses[MachineName::id()])) {
+ return sprintf('E.g. %s/%s-data:latest',
+ Converter::phpNamespace($responses[OrgMachineName::id()]),
+ Converter::phpNamespace($responses[MachineName::id()])
+ );
+ }
+
+ return parent::placeholder($responses);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function shouldRun(array $responses): bool {
+ return $responses[DatabaseDownloadSource::id()] === DatabaseDownloadSource::CONTAINER_REGISTRY;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (
+ isset($responses[OrgMachineName::id()]) &&
+ isset($responses[MachineName::id()]) &&
+ !empty($responses[OrgMachineName::id()]) &&
+ !empty($responses[MachineName::id()])
+ ) {
+ return sprintf(
+ '%s/%s-data:latest',
+ Converter::phpNamespace($responses[OrgMachineName::id()]),
+ Converter::phpNamespace($responses[MachineName::id()])
+ );
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -16,6 +75,20 @@ public function discover(): null|string|bool|array {
return Env::getFromDotenv('VORTEX_DB_IMAGE', $this->dstDir);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Validator::containerImage($v) ? NULL : 'Please enter a valid container image name with an optional tag.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn($v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php b/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php
index f4e0bdf2f..fac20a505 100644
--- a/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php
+++ b/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php
@@ -14,6 +14,38 @@ class DependencyUpdatesProvider extends AbstractHandler {
const RENOVATEBOT_APP = 'renovatebot_app';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'β¬οΈ Dependency updates provider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Use a self-hosted service if you cannot install a GitHub app.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::RENOVATEBOT_CI => 'π€ + π Renovate self-hosted in CI',
+ self::RENOVATEBOT_APP => 'π€ Renovate GitHub app',
+ self::NONE => 'π« None',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return self::RENOVATEBOT_CI;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/DeployType.php b/.vortex/installer/src/Prompts/Handlers/DeployType.php
index 6fa6391da..3245a1db2 100644
--- a/.vortex/installer/src/Prompts/Handlers/DeployType.php
+++ b/.vortex/installer/src/Prompts/Handlers/DeployType.php
@@ -18,6 +18,62 @@ class DeployType extends AbstractHandler {
const WEBHOOK = 'webhook';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Deployment types';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'You can deploy code using one or more methods.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ $options = [
+ self::ARTIFACT => 'π¦ Code artifact',
+ self::LAGOON => 'π Lagoon webhook',
+ self::CONTAINER_IMAGE => 'π³ Container image',
+ self::WEBHOOK => 'π Custom webhook',
+ ];
+
+ // Remove Lagoon option for Acquia hosting.
+ if (isset($responses[HostingProvider::id()]) && $responses[HostingProvider::id()] === HostingProvider::ACQUIA) {
+ unset($options[self::LAGOON]);
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ $defaults = [];
+
+ if (isset($responses[HostingProvider::id()])) {
+ if ($responses[HostingProvider::id()] === HostingProvider::LAGOON) {
+ $defaults[] = self::LAGOON;
+ }
+
+ if ($responses[HostingProvider::id()] === HostingProvider::ACQUIA) {
+ $defaults[] = self::ARTIFACT;
+ }
+ }
+
+ if (empty($defaults)) {
+ $defaults[] = self::WEBHOOK;
+ }
+
+ return $defaults;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Domain.php b/.vortex/installer/src/Prompts/Handlers/Domain.php
index d857b149d..986e9fb2d 100644
--- a/.vortex/installer/src/Prompts/Handlers/Domain.php
+++ b/.vortex/installer/src/Prompts/Handlers/Domain.php
@@ -7,9 +7,49 @@
use DrevOps\VortexInstaller\Utils\Converter;
use DrevOps\VortexInstaller\Utils\Env;
use DrevOps\VortexInstaller\Utils\File;
+use DrevOps\VortexInstaller\Utils\Validator;
class Domain extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Public domain';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Domain name without protocol and trailing slash.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. example.com';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[MachineName::id()]) && !empty($responses[MachineName::id()])) {
+ return Converter::kebab($responses[MachineName::id()]) . '.com';
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -23,6 +63,20 @@ public function discover(): null|string|bool|array {
return NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Validator::domain($v) ? NULL : 'Please enter a valid domain name.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => Converter::domain($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/GithubRepo.php b/.vortex/installer/src/Prompts/Handlers/GithubRepo.php
index 48568cb27..42d331892 100644
--- a/.vortex/installer/src/Prompts/Handlers/GithubRepo.php
+++ b/.vortex/installer/src/Prompts/Handlers/GithubRepo.php
@@ -5,9 +5,54 @@
namespace DrevOps\VortexInstaller\Prompts\Handlers;
use DrevOps\VortexInstaller\Utils\Git;
+use DrevOps\VortexInstaller\Utils\Validator;
class GithubRepo extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π·οΈ What is your GitHub project name?';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name to create new or find an existing repository.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. myorg/myproject';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function shouldRun(array $responses): bool {
+ return !empty($responses[GithubToken::id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (
+ isset($responses[OrgMachineName::id()]) &&
+ isset($responses[MachineName::id()]) &&
+ !empty($responses[OrgMachineName::id()]) &&
+ !empty($responses[MachineName::id()])
+ ) {
+ return $responses[OrgMachineName::id()] . '/' . $responses[MachineName::id()];
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -28,6 +73,20 @@ public function discover(): null|string|bool|array {
return Git::extractOwnerRepo($remote);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn(string $v): ?string => !empty($v) && !Validator::githubProject($v) ? 'Please enter a valid project name in the format "myorg/myproject"' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/GithubToken.php b/.vortex/installer/src/Prompts/Handlers/GithubToken.php
index 679d8bc67..d20f9454a 100644
--- a/.vortex/installer/src/Prompts/Handlers/GithubToken.php
+++ b/.vortex/installer/src/Prompts/Handlers/GithubToken.php
@@ -8,6 +8,37 @@
class GithubToken extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ if (!empty($this->discover())) {
+ return 'GitHub access token is already set in the environment.';
+ }
+ return 'π GitHub access token (optional)';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return Env::get('GITHUB_TOKEN') ? 'Read from GITHUB_TOKEN environment variable.' : 'Create a new token with "repo" scopes at https://github.com/settings/tokens/new';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. ghp_1234567890';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function shouldRun(array $responses): bool {
+ return $responses[CodeProvider::id()] === CodeProvider::GITHUB;
+ }
+
/**
* {@inheritdoc}
*/
@@ -15,6 +46,38 @@ public function discover(): null|string|bool|array {
return Env::getFromDotenv('GITHUB_TOKEN', $this->dstDir);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => !empty($v) && !str_starts_with($v, 'ghp_') ? 'Please enter a valid token starting with "ghp_"' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolvedValue(array $responses): null|string|bool|array {
+ $discovered = $this->discover();
+ if (!empty($discovered)) {
+ return $discovered;
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolvedMessage(array $responses): ?string {
+ return 'GitHub access token is already set in the environment.';
+ }
+
/**
* {@inheritdoc}
*/
@@ -22,4 +85,18 @@ public function process(): void {
// @todo Implement this.
}
+ /**
+ * Get the informational note for GitHub token requirement.
+ */
+ public static function description(array $responses): ?string {
+ return "We need a token to create repositories and manage webhooks.\nIt won't be saved anywhere in the file system.\nYou may skip entering the token, but then Vortex will have to skip several operations.";
+ }
+
+ /**
+ * Get the discovered value for display.
+ */
+ public function getDiscoveredValue(): string {
+ return '';
+ }
+
}
diff --git a/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php b/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php
index c7a07fc46..820b0e1b2 100644
--- a/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php
+++ b/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php
@@ -18,6 +18,85 @@ interface HandlerInterface {
*/
public static function id(): string;
+ /**
+ * Label for of the handler.
+ *
+ * @return string
+ * The label for the handler.
+ */
+ public function label(): string;
+
+ /**
+ * Optional description for the handler.
+ *
+ * @param array $responses
+ * Array of collected responses.
+ *
+ * @return string|null
+ * The description text, or NULL if not applicable.
+ */
+ public static function description(array $responses): ?string;
+
+ /**
+ * Optional hint for the handler.
+ *
+ * @param array $responses
+ * Array of collected responses.
+ *
+ * @return string|null
+ * The hint text for the handler, or NULL if none.
+ */
+ public function hint(array $responses): ?string;
+
+ /**
+ * Optional placeholder.
+ *
+ * @return string|null
+ * The placeholder text for the handler, or NULL if none.
+ */
+ public function placeholder(array $responses): ?string;
+
+ /**
+ * Get whether the handler's value is required.
+ *
+ * @return bool
+ * TRUE if the handler is required, FALSE otherwise.
+ */
+ public function isRequired(): bool;
+
+ /**
+ * Get the options for select/multiselect handlers.
+ *
+ * @param array $responses
+ * Array of collected responses.
+ *
+ * @return array|null
+ * The options array, or null if not applicable.
+ */
+ public function options(array $responses): ?array;
+
+ /**
+ * Check if the handler should run based on collected responses.
+ *
+ * @param array $responses
+ * Array of collected responses.
+ *
+ * @return bool
+ * The condition callback, or null if not conditional.
+ */
+ public function shouldRun(array $responses): bool;
+
+ /**
+ * The default value for the handler.
+ *
+ * @param array $responses
+ * Array of collected responses.
+ *
+ * @return string|bool|array|null
+ * The default value for the handler.
+ */
+ public function default(array $responses): null|string|bool|array;
+
/**
* Discover the value from the environment.
*
@@ -27,9 +106,51 @@ public static function id(): string;
public function discover(): null|string|bool|array;
/**
- * Process the discovered value once all the responses are collected.
+ * The validate callback.
+ *
+ * @return callable|null
+ * The validate callback, or null if none.
*/
- public function process(): void;
+ public function validate(): ?callable;
+
+ /**
+ * The transform callback.
+ *
+ * @return callable|null
+ * The transform callback, or null if none.
+ */
+ public function transform(): ?callable;
+
+ /**
+ * Get a resolved value if this handler's value is already determined.
+ *
+ * If this returns a non-empty value, the caller should use this value
+ * instead of prompting the user for input. This allows handlers to
+ * encapsulate logic for when values are discovered from environment,
+ * auto-selected based on other responses, or otherwise pre-determined.
+ *
+ * @param array $responses
+ * Current form responses for context-aware resolution.
+ *
+ * @return string|bool|array|null
+ * The resolved value if determined, null/empty if user input is needed.
+ */
+ public function resolvedValue(array $responses): null|string|bool|array;
+
+ /**
+ * Get a message to display when showing the resolved value.
+ *
+ * This is used by handlerManager to show an appropriate message (via
+ * info(), ok(), etc.) when using a resolved value instead of handlering
+ * for input.
+ *
+ * @param array $responses
+ * Current form responses for context-aware message generation.
+ *
+ * @return string|null
+ * The message to display, or null if no message needed.
+ */
+ public function resolvedMessage(array $responses): ?string;
/**
* Set the responses.
@@ -51,4 +172,9 @@ public function setResponses(array $responses): static;
*/
public function setWebroot(string $webroot): static;
+ /**
+ * Process the discovered value once all the responses are collected.
+ */
+ public function process(): void;
+
}
diff --git a/.vortex/installer/src/Prompts/Handlers/HostingProvider.php b/.vortex/installer/src/Prompts/Handlers/HostingProvider.php
index 8f8132603..4d15fd832 100644
--- a/.vortex/installer/src/Prompts/Handlers/HostingProvider.php
+++ b/.vortex/installer/src/Prompts/Handlers/HostingProvider.php
@@ -17,6 +17,46 @@ class HostingProvider extends AbstractHandler {
const OTHER = 'other';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'βοΈ Hosting provider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Select the hosting provider where the project is hosted. The web root directory will be set accordingly.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::ACQUIA => 'π§ Acquia Cloud',
+ self::LAGOON => 'π Lagoon',
+ self::OTHER => 'π§© Other',
+ self::NONE => 'π« None',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return 'none';
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Internal.php b/.vortex/installer/src/Prompts/Handlers/Internal.php
index 70916acbd..27bf01370 100644
--- a/.vortex/installer/src/Prompts/Handlers/Internal.php
+++ b/.vortex/installer/src/Prompts/Handlers/Internal.php
@@ -11,6 +11,13 @@
class Internal extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'Internal processing';
+ }
+
public function discover(): null|string|bool|array {
// Noop.
return NULL;
diff --git a/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php b/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php
index f18925632..2aca3e824 100644
--- a/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php
+++ b/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php
@@ -6,6 +6,27 @@
class LabelMergeConflictsPr extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π« Auto-add a CONFLICT label to a PR when conflicts occur?';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Helps to keep quickly identify PRs that need attention.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return TRUE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/MachineName.php b/.vortex/installer/src/Prompts/Handlers/MachineName.php
index 55624b846..e0875d62c 100644
--- a/.vortex/installer/src/Prompts/Handlers/MachineName.php
+++ b/.vortex/installer/src/Prompts/Handlers/MachineName.php
@@ -10,6 +10,45 @@
class MachineName extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π·οΈ Site machine name';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name for the project directory and in the code.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. my_site';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[Name::id()]) && !empty($responses[Name::id()])) {
+ return Converter::machineExtended($responses[Name::id()]);
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -23,6 +62,20 @@ public function discover(): null|string|bool|array {
return NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php b/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php
index 459329e36..c78326a5e 100644
--- a/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php
+++ b/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php
@@ -9,6 +9,45 @@
class ModulePrefix extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π§© Module prefix';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name for custom modules.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. ms (for My Site)';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[MachineName::id()]) && !empty($responses[MachineName::id()])) {
+ return Converter::abbreviation(Converter::machine($responses[MachineName::id()]), 4, ['_']);
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -27,6 +66,20 @@ public function discover(): null|string|bool|array {
return empty($path) ? NULL : str_replace('_base', '', basename($path));
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Converter::machine($v) !== $v ? 'Please enter a valid module prefix: only lowercase letters, numbers, and underscores are allowed.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Name.php b/.vortex/installer/src/Prompts/Handlers/Name.php
index bc3340ad5..55d424157 100644
--- a/.vortex/installer/src/Prompts/Handlers/Name.php
+++ b/.vortex/installer/src/Prompts/Handlers/Name.php
@@ -5,10 +5,47 @@
namespace DrevOps\VortexInstaller\Prompts\Handlers;
use DrevOps\VortexInstaller\Utils\Composer;
+use DrevOps\VortexInstaller\Utils\Converter;
+use DrevOps\VortexInstaller\Utils\Env;
use DrevOps\VortexInstaller\Utils\File;
class Name extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π·οΈ Site name';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name in the project and in the documentation.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. My Site';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return Converter::label(Env::get('VORTEX_PROJECT', basename((string) $this->config->getDst())));
+ }
+
/**
* {@inheritdoc}
*/
@@ -22,6 +59,20 @@ public function discover(): null|string|bool|array {
return NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid project name.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Org.php b/.vortex/installer/src/Prompts/Handlers/Org.php
index b0ca0b72a..3e701a9a6 100644
--- a/.vortex/installer/src/Prompts/Handlers/Org.php
+++ b/.vortex/installer/src/Prompts/Handlers/Org.php
@@ -5,10 +5,50 @@
namespace DrevOps\VortexInstaller\Prompts\Handlers;
use DrevOps\VortexInstaller\Utils\Composer;
+use DrevOps\VortexInstaller\Utils\Converter;
use DrevOps\VortexInstaller\Utils\File;
class Org extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π’ Organization name';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name in the project and in the documentation.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. My Org';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[Name::id()]) && !empty($responses[Name::id()])) {
+ return Converter::label($responses[Name::id()]) . ' Org';
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -22,6 +62,20 @@ public function discover(): null|string|bool|array {
return NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid organization name.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php b/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php
index cdee3318b..2f6a330f0 100644
--- a/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php
+++ b/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php
@@ -5,10 +5,50 @@
namespace DrevOps\VortexInstaller\Prompts\Handlers;
use DrevOps\VortexInstaller\Utils\Composer;
+use DrevOps\VortexInstaller\Utils\Converter;
use DrevOps\VortexInstaller\Utils\File;
class OrgMachineName extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π’ Organization machine name';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name for the project directory and in the code.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. my_org';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[Org::id()]) && !empty($responses[Org::id()])) {
+ return Converter::machineExtended($responses[Org::id()]);
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -22,6 +62,20 @@ public function discover(): null|string|bool|array {
return NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid organisation machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php b/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php
index aabee43d2..d49326d02 100644
--- a/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php
+++ b/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php
@@ -8,6 +8,27 @@
class PreserveDocsOnboarding extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Preserve onboarding checklist?';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Helps to track onboarding to Vortex within the repository.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return TRUE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php b/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php
index e8f3435aa..250f419d7 100644
--- a/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php
+++ b/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php
@@ -8,6 +8,27 @@
class PreserveDocsProject extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Preserve project documentation?';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Helps to maintain the project documentation within the repository.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return TRUE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Profile.php b/.vortex/installer/src/Prompts/Handlers/Profile.php
index 73958e42e..8b60dc1fa 100644
--- a/.vortex/installer/src/Prompts/Handlers/Profile.php
+++ b/.vortex/installer/src/Prompts/Handlers/Profile.php
@@ -20,28 +20,54 @@ class Profile extends AbstractHandler {
/**
* {@inheritdoc}
*/
- public function discover(): null|string|bool|array {
- if ($this->isInstalled()) {
- $value = Env::getFromDotenv('DRUPAL_PROFILE', $this->dstDir);
- if (!empty($value)) {
- return $value;
- }
- }
+ public function label(): string {
+ return 'π§Ύ Profile';
+ }
- $locations = [
- $this->dstDir . sprintf('/%s/profiles/*/*.info', $this->webroot),
- $this->dstDir . sprintf('/%s/profiles/*/*.info.yml', $this->webroot),
- $this->dstDir . sprintf('/%s/profiles/custom/*/*.info', $this->webroot),
- $this->dstDir . sprintf('/%s/profiles/custom/*/*.info.yml', $this->webroot),
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Select which profile to use';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::STANDARD => 'Standard',
+ self::MINIMAL => 'Minimal',
+ self::DEMO_UMAMI => 'Demo Umami',
+ self::CUSTOM => 'Custom',
];
+ }
- $path = File::findMatchingPath($locations);
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return self::STANDARD;
+ }
- if (empty($path)) {
- return NULL;
+ /**
+ * {@inheritdoc}
+ */
+ public function discover(): null|string|bool|array {
+ $value = $this->discoverName();
+
+ if (!is_null($value)) {
+ return in_array($value, [self::STANDARD, self::MINIMAL, self::DEMO_UMAMI]) ? $value : self::CUSTOM;
}
- return str_replace(['.info.yml', '.info'], '', basename($path));
+ return NULL;
}
/**
@@ -49,6 +75,12 @@ public function discover(): null|string|bool|array {
*/
public function process(): void {
$v = $this->getResponseAsString();
+
+ // If user selected 'custom', use the ProfileCustom response instead.
+ if ($v === self::CUSTOM && isset($this->responses['profile_custom'])) {
+ $v = $this->responses['profile_custom'];
+ }
+
$t = $this->tmpDir;
$w = $this->webroot;
@@ -69,4 +101,34 @@ public function process(): void {
}
}
+ /**
+ * Discover the profile name from the filesystem or environment.
+ *
+ * @return null|string|bool|array
+ * The profile name if found, NULL if not found.
+ */
+ public function discoverName(): null|string|bool|array {
+ if ($this->isInstalled()) {
+ $value = Env::getFromDotenv('DRUPAL_PROFILE', $this->dstDir);
+ if (!empty($value)) {
+ return $value;
+ }
+ }
+
+ $locations = [
+ $this->dstDir . sprintf('/%s/profiles/*/*.info', $this->webroot),
+ $this->dstDir . sprintf('/%s/profiles/*/*.info.yml', $this->webroot),
+ $this->dstDir . sprintf('/%s/profiles/custom/*/*.info', $this->webroot),
+ $this->dstDir . sprintf('/%s/profiles/custom/*/*.info.yml', $this->webroot),
+ ];
+
+ $path = File::findMatchingPath($locations);
+
+ if (empty($path)) {
+ return NULL;
+ }
+
+ return str_replace(['.info.yml', '.info'], '', basename($path));
+ }
+
}
diff --git a/.vortex/installer/src/Prompts/Handlers/ProfileCustom.php b/.vortex/installer/src/Prompts/Handlers/ProfileCustom.php
new file mode 100644
index 000000000..88d037c1b
--- /dev/null
+++ b/.vortex/installer/src/Prompts/Handlers/ProfileCustom.php
@@ -0,0 +1,86 @@
+config);
+ $profile_handler->setWebroot($this->webroot);
+ $discovered = $profile_handler->discoverName();
+
+ // Only return discovered value if it's a custom profile.
+ if (!empty($discovered) && !in_array($discovered, [Profile::STANDARD, Profile::MINIMAL, Profile::DEMO_UMAMI])) {
+ return $discovered;
+ }
+
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn(string $v): ?string => !empty($v) && Converter::machine($v) !== $v ?
+ 'Please enter a valid profile name: only lowercase letters, numbers, and underscores are allowed.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function process(): void {
+ // This handler doesn't need processing - the Profile handler will handle
+ // the final result.
+ }
+
+}
diff --git a/.vortex/installer/src/Prompts/Handlers/ProvisionType.php b/.vortex/installer/src/Prompts/Handlers/ProvisionType.php
index 602449c8a..d172aad67 100644
--- a/.vortex/installer/src/Prompts/Handlers/ProvisionType.php
+++ b/.vortex/installer/src/Prompts/Handlers/ProvisionType.php
@@ -13,6 +13,37 @@ class ProvisionType extends AbstractHandler {
const PROFILE = 'profile';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π¦ Provision type';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Selecting "Profile" will install site from a profile rather than a database dump.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::DATABASE => 'Import from database dump',
+ self::PROFILE => 'Install from profile',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return self::DATABASE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Services.php b/.vortex/installer/src/Prompts/Handlers/Services.php
index cdd65f1a0..fbbb27bd2 100644
--- a/.vortex/installer/src/Prompts/Handlers/Services.php
+++ b/.vortex/installer/src/Prompts/Handlers/Services.php
@@ -15,6 +15,38 @@ class Services extends AbstractHandler {
const SOLR = 'solr';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Services';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Select the services you want to use in the project.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function options(array $responses): ?array {
+ return [
+ self::CLAMAV => 'π¦ ClamAV',
+ self::SOLR => 'π Solr',
+ self::VALKEY => 'ποΈ Valkey',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ return [self::CLAMAV, self::SOLR, self::VALKEY];
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Theme.php b/.vortex/installer/src/Prompts/Handlers/Theme.php
index a0c2bb72e..2a219df5d 100644
--- a/.vortex/installer/src/Prompts/Handlers/Theme.php
+++ b/.vortex/installer/src/Prompts/Handlers/Theme.php
@@ -10,6 +10,38 @@
class Theme extends AbstractHandler {
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π¨ Theme machine name';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'We will use this name for the theme directory. Leave empty to skip the theme scaffold.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. mytheme';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ if (isset($responses[MachineName::id()]) && !empty($responses[MachineName::id()])) {
+ return Converter::machine($responses[MachineName::id()]);
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -30,6 +62,20 @@ public function discover(): null|string|bool|array {
return str_replace(['.info.yml', '.info'], '', basename($path));
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => !empty($v) && Converter::machine($v) !== $v ? 'Please enter a valid theme machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => trim($v);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/Handlers/Webroot.php b/.vortex/installer/src/Prompts/Handlers/Webroot.php
index cc0559974..3e0ce1d61 100644
--- a/.vortex/installer/src/Prompts/Handlers/Webroot.php
+++ b/.vortex/installer/src/Prompts/Handlers/Webroot.php
@@ -7,6 +7,7 @@
use DrevOps\VortexInstaller\Utils\Composer;
use DrevOps\VortexInstaller\Utils\Env;
use DrevOps\VortexInstaller\Utils\File;
+use DrevOps\VortexInstaller\Utils\Validator;
class Webroot extends AbstractHandler {
@@ -14,6 +15,50 @@ class Webroot extends AbstractHandler {
const DOCROOT = 'docroot';
+ /**
+ * {@inheritdoc}
+ */
+ public function label(): string {
+ return 'π Custom web root directory';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hint(array $responses): ?string {
+ return 'Custom directory where the web server serves the site.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function placeholder(array $responses): ?string {
+ return 'E.g. ' . implode(', ', [self::WEB, self::DOCROOT]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired(): bool {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function default(array $responses): null|string|bool|array {
+ // Auto-select webroot based on hosting provider.
+ if (isset($responses[HostingProvider::id()])) {
+ return match ($responses[HostingProvider::id()]) {
+ HostingProvider::ACQUIA => self::DOCROOT,
+ HostingProvider::LAGOON => self::WEB,
+ default => self::WEB,
+ };
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
@@ -31,6 +76,45 @@ public function discover(): null|string|bool|array {
return $value;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): ?callable {
+ return fn($v): ?string => Validator::dirname($v) ? NULL : 'Please enter a valid webroot name: only lowercase letters, numbers, and underscores are allowed.';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function transform(): ?callable {
+ return fn(string $v): string => rtrim($v, DIRECTORY_SEPARATOR);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolvedValue(array $responses): null|string|bool|array {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolvedMessage(array $responses): ?string {
+ if (
+ isset($responses[HostingProvider::id()]) &&
+ $responses[HostingProvider::id()] !== HostingProvider::OTHER
+ ) {
+ $webroot = $this->default($responses);
+ if (is_array($webroot)) {
+ throw new \InvalidArgumentException('Web root must be a string, got: ' . gettype($webroot));
+ }
+ return sprintf('Web root will be set to "%s".', (string) $webroot);
+ }
+
+ return NULL;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/.vortex/installer/src/Prompts/PromptManager.php b/.vortex/installer/src/Prompts/PromptManager.php
index 65723ccf3..dfbde6db1 100644
--- a/.vortex/installer/src/Prompts/PromptManager.php
+++ b/.vortex/installer/src/Prompts/PromptManager.php
@@ -28,6 +28,7 @@
use DrevOps\VortexInstaller\Prompts\Handlers\PreserveDocsOnboarding;
use DrevOps\VortexInstaller\Prompts\Handlers\PreserveDocsProject;
use DrevOps\VortexInstaller\Prompts\Handlers\Profile;
+use DrevOps\VortexInstaller\Prompts\Handlers\ProfileCustom;
use DrevOps\VortexInstaller\Prompts\Handlers\ProvisionType;
use DrevOps\VortexInstaller\Prompts\Handlers\Services;
use DrevOps\VortexInstaller\Prompts\Handlers\Theme;
@@ -36,7 +37,6 @@
use DrevOps\VortexInstaller\Utils\Converter;
use DrevOps\VortexInstaller\Utils\Env;
use DrevOps\VortexInstaller\Utils\Tui;
-use DrevOps\VortexInstaller\Utils\Validator;
use Symfony\Component\Console\Output\OutputInterface;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\form;
@@ -55,24 +55,24 @@
*/
class PromptManager {
- /**
- * Array of responses.
- */
- protected array $responses = [];
-
/**
* Total number of top-level responses.
*
* Used to display the progress of the prompts.
*/
- protected int $totalResponses = 21;
+ const TOTAL_RESPONSES = 21;
+
+ /**
+ * Array of responses.
+ */
+ protected array $responses = [];
/**
- * Current response number.
+ * Current response index.
*
* Used to display the progress of the prompts.
*/
- protected int $currentResponse = 0;
+ protected int $currentResponseIndex = 0;
/**
* Array of handlers.
@@ -94,12 +94,14 @@ public function __construct(
}
/**
- * Prompt for responses.
+ * Run prompts to get responses.
*
* If non-interactive mode is used, the values provided by $this->default()
* method, including discovery from the existing codebase, will be used.
*/
- public function prompt(): void {
+ public function runPrompts(): void {
+ // Set verbosity for TUI output based on the config. This will be reset
+ // after the prompt is completed.
$original_verbosity = Tui::output()->getVerbosity();
if ($this->config->getNoInteraction()) {
Tui::output()->setVerbosity(OutputInterface::VERBOSITY_QUIET);
@@ -112,368 +114,112 @@ public function prompt(): void {
// phpcs:disable Drupal.WhiteSpace.ScopeIndent.IncorrectExact
$responses = form()
->intro('General information')
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π·οΈ Site name'),
- hint: 'We will use this name in the project and in the documentation.',
- placeholder: 'E.g. My Site',
- required: TRUE,
- default: $this->default($n, Converter::label(Env::get('VORTEX_PROJECT', basename((string) $this->config->getDst())))),
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid project name.' : NULL,
- ), Name::id())
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π·οΈ Site machine name'),
- hint: 'We will use this name for the project directory and in the code.',
- placeholder: 'E.g. my_site',
- required: TRUE,
- default: $this->default($n, Converter::machineExtended($r[Name::id()])),
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL,
- ), MachineName::id())
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π’ Organization name'),
- hint: 'We will use this name in the project and in the documentation.',
- placeholder: 'E.g. My Org',
- required: TRUE,
- default: $this->default('org', Converter::label($r[Name::id()]) . ' Org'),
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid organization name.' : NULL,
- ), Org::id())
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π’ Organization machine name'),
- hint: 'We will use this name for the project directory and in the code.',
- placeholder: 'E.g. my_org',
- required: TRUE,
- default: $this->default($n, Converter::machineExtended($r[Org::id()])),
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid organisation machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL,
- ), OrgMachineName::id())
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π Public domain'),
- hint: 'Domain name without protocol and trailing slash.',
- placeholder: 'E.g. example.com',
- required: TRUE,
- default: $this->default($n, Converter::kebab($r[MachineName::id()]) . '.com'),
- transform: fn(string $v): string => Converter::domain($v),
- validate: fn($v): ?string => Validator::domain($v) ? NULL : 'Please enter a valid domain name.',
- ), Domain::id())
+ ->add(fn($r, $pr, $n): string => text(...$this->args(Name::class)), Name::id())
+ ->add(fn($r, $pr, $n): string => text(...$this->args(MachineName::class, NULL, $r)), MachineName::id())
+ ->add(fn($r, $pr, $n): string => text(...$this->args(Org::class, NULL, $r)), Org::id())
+ ->add(fn($r, $pr, $n): string => text(...$this->args(OrgMachineName::class, NULL, $r)), OrgMachineName::id())
+ ->add(fn($r, $pr, $n): string => text(...$this->args(Domain::class, NULL, $r)), Domain::id())
->intro('Code repository')
-
- ->add(fn($r, $pr, $n): int|string => select(
- label: $this->label('ποΈ Repository provider'),
- hint: 'Vortex offers full automation with GitHub, while support for other providers is limited.',
- options: [
- CodeProvider::GITHUB => 'GitHub',
- CodeProvider::OTHER => 'Other',
- ],
- default: $this->default($n, 'github'),
- ), CodeProvider::id())
-
+ ->add(fn($r, $pr, $n): int|string => select(...$this->args(CodeProvider::class)), CodeProvider::id())
->addIf(
- fn($r): bool => $r[CodeProvider::id()] === CodeProvider::GITHUB,
- fn($r, $pr, $n) => Tui::note("We need a token to create repositories and manage webhooks.\nIt won't be saved anywhere in the file system.\nYou may skip entering the token, but then Vortex will have to skip several operations."),
+ fn($r): bool => $this->handlers[GithubToken::id()]->shouldRun($r),
+ fn($r, $pr, $n) => Tui::note('' . GithubToken::description($r) . '')
)
-
->addIf(
- fn($r): bool => $r[CodeProvider::id()] === CodeProvider::GITHUB,
+ fn($r): bool => $this->handlers[GithubToken::id()]->shouldRun($r),
function ($r, $pr, $n): string {
- $value = $this->default($n);
- if (!empty($value)) {
- Tui::ok($this->label('GitHub access token is already set in the environment.', 'a'));
-
- return $value;
+ $handler = $this->handlers[GithubToken::id()];
+ $resolved_value = $handler->resolvedValue($r);
+ if (!empty($resolved_value)) {
+ Tui::ok($this->label((string) $handler->resolvedMessage($r), 'a'));
+ return is_string($resolved_value) ? $resolved_value : '';
}
-
- return password(
- label: $this->label('π GitHub access token (optional)', 'a'),
- hint: Env::get('GITHUB_TOKEN') ? 'Read from GITHUB_TOKEN environment variable.' : 'Create a new token with "repo" scopes at https://github.com/settings/tokens/new',
- placeholder: 'E.g. ghp_1234567890',
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => !empty($v) && !str_starts_with($v, 'ghp_') ? 'Please enter a valid token starting with "ghp_"' : NULL,
- );
- }, GithubToken::id())
-
+ else {
+ return password(...$this->args(GithubToken::class));
+ }
+ },
+ GithubToken::id()
+ )
->addIf(
- fn($r): bool => !empty($r[GithubToken::id()]),
- fn($r, $pr, $n): string => text(
- label: $this->label('π·οΈ What is your GitHub project name?', 'b'),
- hint: 'We will use this name to create new or find an existing repository.',
- placeholder: 'E.g. myorg/myproject',
- default: $this->default($n, $r[OrgMachineName::id()] . '/' . $r[MachineName::id()]),
- transform: fn(string $v): string => trim($v),
- validate: fn(string $v): ?string => !empty($v) && !Validator::githubProject($v) ? 'Please enter a valid project name in the format "myorg/myproject"' : NULL,
- ), GithubRepo::id())
+ fn($r): bool => $this->handlers[GithubRepo::id()]->shouldRun($r),
+ fn($r, $pr, $n): string => text(...$this->args(GithubRepo::class, NULL, $r)),
+ GithubRepo::id()
+ )
->intro('Drupal')
-
- ->add(function ($r, $pr, $n): int|string {
- $profile = select(
- label: $this->label('π§Ύ Profile'),
- hint: 'Select which profile to use',
- options: [
- Profile::STANDARD => 'Standard',
- Profile::MINIMAL => 'Minimal',
- Profile::DEMO_UMAMI => 'Demo Umami',
- Profile::CUSTOM => 'Custom',
- ],
- required: TRUE,
- default: empty($this->default($n)) ? Profile::STANDARD : Profile::CUSTOM,
- );
-
- if ($profile === Profile::CUSTOM) {
- $profile = text(
- label: $this->label('π§Ύ Custom profile machine name', 'a'),
- placeholder: 'E.g. my_profile',
- required: TRUE,
- default: $this->default($n),
- transform: fn(string $v): string => trim($v),
- validate: fn(string $v): ?string => !empty($v) && Converter::machine($v) !== $v ? 'Please enter a valid profile name: only lowercase letters, numbers, and underscores are allowed.' : NULL,
- );
- }
-
- return $profile;
- }, Profile::id())
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π§© Module prefix'),
- hint: 'We will use this name for custom modules.',
- placeholder: 'E.g. ms (for My Site)',
- required: TRUE,
- default: $this->default($n, Converter::abbreviation(Converter::machine($r[MachineName::id()]), 4, ['_'])),
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => Converter::machine($v) !== $v ? 'Please enter a valid module prefix: only lowercase letters, numbers, and underscores are allowed.' : NULL,
- ), ModulePrefix::id())
-
- ->add(fn($r, $pr, $n): string => text(
- label: $this->label('π¨ Theme machine name'),
- hint: 'We will use this name for the theme directory. Leave empty to skip the theme scaffold.',
- placeholder: 'E.g. mytheme',
- default: $this->default($n, Converter::machine($r[MachineName::id()])),
- transform: fn(string $v): string => trim($v),
- validate: fn($v): ?string => !empty($v) && Converter::machine($v) !== $v ? 'Please enter a valid theme machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL,
- ), Theme::id())
+ ->add(
+ fn($r, $pr, $n): int|string => select(...$this->args(Profile::class)),
+ Profile::id()
+ )
+ ->addIf(
+ fn($r): bool => $this->handlers[ProfileCustom::id()]->shouldRun($r),
+ fn($r, $pr, $n): string => text(...$this->args(ProfileCustom::class)),
+ ProfileCustom::id()
+ )
+ ->add(fn($r, $pr, $n): string => text(...$this->args(ModulePrefix::class, NULL, $r)), ModulePrefix::id())
+ ->add(fn($r, $pr, $n): string => text(...$this->args(Theme::class, NULL, $r)), Theme::id())
->intro('Services')
-
- ->add(fn($r, $pr, $n): array => multiselect(
- label: $this->label('π Services'),
- hint: 'Select the services you want to use in the project.',
- options: [
- Services::CLAMAV => 'π¦ ClamAV',
- Services::SOLR => 'π Solr',
- Services::VALKEY => 'ποΈ Valkey',
- ],
- default: $this->default($n, [Services::CLAMAV, Services::SOLR, Services::VALKEY]),
- ), Services::id())
+ ->add(fn($r, $pr, $n): array => multiselect(...$this->args(Services::class)), Services::id())
->intro('Hosting')
-
- ->add(fn($r, $pr, $n): int|string => select(
- label: $this->label('βοΈ Hosting provider'),
- hint: 'Select the hosting provider where the project is hosted. The web root directory will be set accordingly.',
- options: [
- HostingProvider::ACQUIA => 'π§ Acquia Cloud',
- HostingProvider::LAGOON => 'π Lagoon',
- HostingProvider::OTHER => 'π§© Other',
- HostingProvider::NONE => 'π« None',
- ],
- required: TRUE,
- default: $this->default($n, 'none'),
- ), HostingProvider::id())
-
- ->add(function (array $r, $pr, $n): string|bool|array {
- if ($r[HostingProvider::id()] !== HostingProvider::OTHER) {
- $webroot = match ($r[HostingProvider::id()]) {
- HostingProvider::ACQUIA => Webroot::DOCROOT,
- HostingProvider::LAGOON => Webroot::WEB,
- default => $this->default($n, Webroot::WEB)
- };
-
- info(sprintf('Web root will be set to "%s".', $webroot));
- }
- else {
- $webroot = text(
- label: $this->label('π Custom web root directory', 'a'),
- hint: 'Custom directory where the web server serves the site.',
- placeholder: 'E.g. ' . implode(', ', [Webroot::WEB, Webroot::DOCROOT]),
- required: TRUE,
- default: $this->default($n, Webroot::WEB),
- transform: fn(string $v): string => rtrim($v, DIRECTORY_SEPARATOR),
- validate: fn($v): ?string => Validator::dirname($v) ? NULL : 'Please enter a valid webroot name: only lowercase letters, numbers, and underscores are allowed.',
- );
- }
- return $webroot;
- }, Webroot::id())
+ ->add(fn($r, $pr, $n): int|string => select(...$this->args(HostingProvider::class)), HostingProvider::id())
+ ->add(
+ function (array $r, $pr, $n): string {
+ $handler = $this->handlers[Webroot::id()];
+ $resolved = $handler->resolvedValue($r);
+ if (!is_null($resolved)) {
+ info($handler->resolvedMessage($r));
+ return match (TRUE) {
+ is_string($resolved) => $resolved,
+ is_array($resolved) => json_encode($resolved) ?: 'array',
+ default => 'true',
+ };
+ }
+ else {
+ return text(...$this->args(Webroot::class, NULL, $r));
+ }
+ },
+ Webroot::id()
+ )
->intro('Deployment')
-
- ->add(function (array $r, $pr, $n): array {
- $defaults = [];
-
- $options = [
- DeployType::ARTIFACT => 'π¦ Code artifact',
- DeployType::LAGOON => 'π Lagoon webhook',
- DeployType::CONTAINER_IMAGE => 'π³ Container image',
- DeployType::WEBHOOK => 'π Custom webhook',
- ];
-
- if ($r[HostingProvider::id()] === HostingProvider::LAGOON) {
- $defaults[] = DeployType::LAGOON;
- }
-
- if ($r[HostingProvider::id()] === HostingProvider::ACQUIA) {
- $defaults[] = DeployType::ARTIFACT;
- unset($options[DeployType::LAGOON]);
- }
-
- if (empty($defaults)) {
- $defaults[] = DeployType::WEBHOOK;
- }
-
- return multiselect(
- label: $this->label('π Deployment types'),
- hint: 'You can deploy code using one or more methods.',
- options: $options,
- default: $this->default($n, $defaults),
- );
- }, DeployType::id())
+ ->add(fn($r, $pr, $n): array => multiselect(...$this->args(DeployType::class, NULL, $r)), DeployType::id())
->intro('Workflow')
-
->add(fn($r, $pr, $n) => Tui::note('Provisioning is the process of setting up the site in the environment with an already assembled codebase.'))
-
- ->add(fn($r, $pr, $n): int|string => select(
- label: $this->label('π¦ Provision type'),
- hint: 'Selecting "Profile" will install site from a profile rather than a database dump.',
- options: [
- ProvisionType::DATABASE => 'Import from database dump',
- ProvisionType::PROFILE => 'Install from profile',
- ],
- default: $this->default($n, ProvisionType::DATABASE),
- ), ProvisionType::id())
-
- ->add(function (array $r, $pr, $n): int|string {
- if ($r[ProvisionType::id()] === ProvisionType::PROFILE) {
- return DatabaseDownloadSource::NONE;
- }
-
- $options = [
- DatabaseDownloadSource::URL => 'π URL download',
- DatabaseDownloadSource::FTP => 'π FTP download',
- DatabaseDownloadSource::ACQUIA => 'π§ Acquia backup',
- DatabaseDownloadSource::LAGOON => 'π Lagoon environment',
- DatabaseDownloadSource::CONTAINER_REGISTRY => 'π³ Container registry',
- DatabaseDownloadSource::NONE => 'π« None',
- ];
-
- if ($r[HostingProvider::id()] === HostingProvider::ACQUIA) {
- unset($options[DatabaseDownloadSource::LAGOON]);
- }
-
- if ($r[HostingProvider::id()] === HostingProvider::LAGOON) {
- unset($options[DatabaseDownloadSource::ACQUIA]);
- }
-
- return select(
- label: $this->label('π‘ Database source', 'a'),
- hint: 'The database can be downloaded as an exported dump file or pre-packaged in a container image.',
- options: $options,
- default: $this->default($n, match ($r[HostingProvider::id()]) {
- HostingProvider::ACQUIA => DatabaseDownloadSource::ACQUIA,
- HostingProvider::LAGOON => DatabaseDownloadSource::LAGOON,
- default => DatabaseDownloadSource::URL,
- }),
- );
- }, DatabaseDownloadSource::id())
-
+ ->add(fn($r, $pr, $n): int|string => select(...$this->args(ProvisionType::class)), ProvisionType::id())
->addIf(
- fn($r): bool => $r[DatabaseDownloadSource::id()] === DatabaseDownloadSource::CONTAINER_REGISTRY,
- fn($r, $pr, $n): string => text(
- label: $this->label('π·οΈ What is your database container image name and a tag?', 'a'),
- hint: 'Use "latest" tag for the latest version. CI will be building this image overnight.',
- placeholder: sprintf('E.g. %s/%s-data:latest', Converter::phpNamespace($r[OrgMachineName::id()]), Converter::phpNamespace($r[MachineName::id()])),
- default: $this->default($n, sprintf('%s/%s-data:latest', Converter::phpNamespace($r[OrgMachineName::id()]), Converter::phpNamespace($r[MachineName::id()]))),
- transform: fn($v): string => trim($v),
- validate: fn($v): ?string => Validator::containerImage($v) ? NULL : 'Please enter a valid container image name with an optional tag.',
- ), DatabaseImage::id())
+ fn($r): bool => $this->handlers[DatabaseDownloadSource::id()]->shouldRun($r),
+ fn($r, $pr, $n): int|string => select(...$this->args(DatabaseDownloadSource::class, NULL, $r)),
+ DatabaseDownloadSource::id()
+ )
+ ->addIf(
+ fn($r): bool => $this->handlers[DatabaseImage::id()]->shouldRun($r),
+ function ($r, $pr, $n): string {
+ $handler = $this->handlers[DatabaseImage::id()];
+ $args = $this->args(DatabaseImage::class, NULL, $r);
+ $args['placeholder'] = $handler->placeholder($r);
+ return text(...$args);
+ },
+ DatabaseImage::id()
+ )
->intro('Continuous Integration')
-
- ->add(function (array $r, $pr, $n): int|string {
- $options = [
- CiProvider::NONE => 'None',
- CiProvider::GITHUB_ACTIONS => 'GitHub Actions',
- CiProvider::CIRCLECI => 'CircleCI',
- ];
-
- if ($r[CodeProvider::id()] !== CodeProvider::GITHUB) {
- unset($options[CiProvider::GITHUB_ACTIONS]);
- }
-
- return select(
- label: $this->label('π Continuous Integration provider'),
- hint: 'Both providers support equivalent workflow.',
- options: $options,
- default: $this->default($n, CiProvider::GITHUB_ACTIONS),
- );
- }, CiProvider::id())
+ ->add(fn(array $r, $pr, $n): int|string => select(...$this->args(CiProvider::class, NULL, $r)), CiProvider::id())
->intro('Automations')
-
- ->add(fn($r, $pr, $n): int|string => select(
- label: $this->label('β¬οΈ Dependency updates provider'),
- hint: 'Use a self-hosted service if you canβt install a GitHub app.',
- options: [
- DependencyUpdatesProvider::RENOVATEBOT_CI => 'π€ + π Renovate self-hosted in CI',
- DependencyUpdatesProvider::RENOVATEBOT_APP => 'π€ Renovate GitHub app',
- DependencyUpdatesProvider::NONE => 'π« None',
- ],
- default: $this->default($n, DependencyUpdatesProvider::RENOVATEBOT_CI),
- ), DependencyUpdatesProvider::id())
-
- ->add(fn($r, $pr, $n): bool => confirm(
- label: $this->label('π€ Auto-assign the author to their PR?'),
- hint: 'Helps to keep the PRs organized.',
- default: $this->default($n, TRUE),
- ), AssignAuthorPr::id())
-
- ->add(fn($r, $pr, $n): bool => confirm(
- label: $this->label('π« Auto-add a CONFLICT label to a PR when conflicts occur?'),
- hint: 'Helps to keep quickly identify PRs that need attention.',
- default: $this->default($n, TRUE),
- ), LabelMergeConflictsPr::id())
+ ->add(fn($r, $pr, $n): int|string => select(...$this->args(DependencyUpdatesProvider::class)), DependencyUpdatesProvider::id())
+ ->add(fn($r, $pr, $n): bool => confirm(...$this->args(AssignAuthorPr::class)), AssignAuthorPr::id())
+ ->add(fn($r, $pr, $n): bool => confirm(...$this->args(LabelMergeConflictsPr::class)), LabelMergeConflictsPr::id())
->intro('Documentation')
-
- ->add(fn($r, $pr, $n): bool => confirm(
- label: $this->label('π Preserve project documentation?'),
- hint: 'Helps to maintain the project documentation within the repository.',
- default: $this->default($n, TRUE),
- ), PreserveDocsProject::id())
-
- ->add(fn($r, $pr, $n): bool => confirm(
- label: $this->label('π Preserve onboarding checklist?'),
- hint: 'Helps to track onboarding to Vortex within the repository.',
- default: $this->default($n, TRUE),
- ), PreserveDocsOnboarding::id())
+ ->add(fn($r, $pr, $n): bool => confirm(...$this->args(PreserveDocsProject::class)), PreserveDocsProject::id())
+ ->add(fn($r, $pr, $n): bool => confirm(...$this->args(PreserveDocsOnboarding::class)), PreserveDocsOnboarding::id())
->intro('AI')
-
- ->add(fn($r, $pr, $n): int|string => select(
- label: $this->label('π€ AI code assistant instructions'),
- hint: 'Helps AI coding assistants to understand the project better.',
- options: [
- AiCodeInstructions::CLAUDE => 'Anthropic Claude',
- AiCodeInstructions::NONE => 'None',
- ],
- default: $this->default($n, AiCodeInstructions::NONE),
- ), AiCodeInstructions::id())
+ ->add(fn($r, $pr, $n): int|string => select(...$this->args(AiCodeInstructions::class)), AiCodeInstructions::id())
->submit();
@@ -488,6 +234,19 @@ function ($r, $pr, $n): string {
return !is_numeric($key);
}, ARRAY_FILTER_USE_KEY);
+ // Handle Profile custom name merging.
+ if (isset($responses[Profile::id()]) && $responses[Profile::id()] === Profile::CUSTOM && isset($responses[ProfileCustom::id()])) {
+ $responses[Profile::id()] = $responses[ProfileCustom::id()];
+ }
+
+ // Always remove ProfileCustom key (it's only used for internal merging)
+ unset($responses[ProfileCustom::id()]);
+
+ // Handle DatabaseDownloadSource when ProvisionType is PROFILE.
+ if (isset($responses[ProvisionType::id()]) && $responses[ProvisionType::id()] === ProvisionType::PROFILE) {
+ $responses[DatabaseDownloadSource::id()] = DatabaseDownloadSource::NONE;
+ }
+
if ($this->config->getNoInteraction()) {
Tui::output()->setVerbosity($original_verbosity);
}
@@ -497,6 +256,12 @@ function ($r, $pr, $n): string {
/**
* Get all received responses.
+ *
+ * Used to provide direct access to the responses values.
+ *
+ * @return array
+ * An associative array of responses, where keys are handler IDs and values
+ * are the responses provided by the user or discovered by handlers.
*/
public function getResponses(): array {
return $this->responses;
@@ -505,9 +270,9 @@ public function getResponses(): array {
/**
* Run all processors.
*/
- public function process(): void {
+ public function runProcessors(): void {
// Run processors in the reverse order of how they are defined in the
- // PromptManager to ensure that the handlers for string replacements process
+ // runPrompts() to ensure that the handlers for string replacements process
// more specific values first, and the more generic ones last.
$ids = [
Webroot::id(),
@@ -525,7 +290,9 @@ public function process(): void {
HostingProvider::id(),
Services::id(),
GithubRepo::id(),
+ GithubToken::id(),
CodeProvider::id(),
+ ProfileCustom::id(),
Profile::id(),
Domain::id(),
ModulePrefix::id(),
@@ -547,6 +314,15 @@ public function process(): void {
}
}
+ /**
+ * Check if the installation should proceed.
+ *
+ * This method checks the configuration for the no-interaction mode and
+ * prompts the user for confirmation if not in no-interaction mode.
+ *
+ * @return bool
+ * TRUE if the installation should proceed, FALSE otherwise.
+ */
public function shouldProceed(): bool {
$proceed = TRUE;
@@ -557,7 +333,7 @@ public function shouldProceed(): bool {
);
}
- // Kill-switch to not proceed with install. If false, the installer will not
+ // Kill-switch to not proceed with install. If FALSE, the installer will not
// proceed despite the answer received above.
if (!$this->config->get(Config::PROCEED)) {
$proceed = FALSE;
@@ -566,12 +342,8 @@ public function shouldProceed(): bool {
return $proceed;
}
- public static function makeEnvName(string $id): string {
- return Converter::constant('VORTEX_INSTALL_PROMPT_' . $id);
- }
-
public function getResponsesSummary(): array {
- $responses = $this->getResponses();
+ $responses = $this->responses;
$values['General information'] = Tui::LIST_SECTION_TITLE;
$values['π·οΈ Site name'] = $responses[Name::id()];
@@ -641,6 +413,19 @@ public function getResponsesSummary(): array {
return $values;
}
+ /**
+ * Generate an environment variable name for a prompt.
+ *
+ * @param string $id
+ * The prompt ID.
+ *
+ * @return string
+ * The environment variable name.
+ */
+ public static function makeEnvName(string $id): string {
+ return Converter::constant('VORTEX_INSTALL_PROMPT_' . $id);
+ }
+
/**
* Generate a label for a prompt.
*
@@ -654,37 +439,12 @@ public function getResponsesSummary(): array {
*/
protected function label(string $text, ?string $suffix = NULL): string {
if (is_null($suffix)) {
- $this->currentResponse++;
- }
-
- $suffix = $suffix !== NULL ? $this->currentResponse . '.' . $suffix : $this->currentResponse;
-
- return $text . ' ' . Tui::dim('(' . $suffix . '/' . $this->totalResponses . ')');
- }
-
- /**
- * Get a default value for a response.
- *
- * @param string $id
- * The response name.
- * @param string $default
- * The default value to return.
- */
- protected function default(string $id, string|bool|array $default = ''): mixed {
- // Allow to set the value from the environment variable.
- $env_var = static::makeEnvName($id);
- $env_val = getenv($env_var);
- if (is_string($env_val)) {
- return Env::toValue($env_val);
+ $this->currentResponseIndex++;
}
- if (!array_key_exists($id, $this->handlers)) {
- return $default;
- }
-
- $discovered = $this->handlers[$id]->discover();
+ $suffix = $suffix !== NULL ? $this->currentResponseIndex . '.' . $suffix : $this->currentResponseIndex;
- return is_null($discovered) ? $default : $discovered;
+ return $text . ' ' . Tui::dim('(' . $suffix . '/' . static::TOTAL_RESPONSES . ')');
}
/**
@@ -714,11 +474,11 @@ protected function initHandlers(): void {
$classes[] = $class;
}
- // Discover webroot and set for all handlers to help with paths resolution.
+ // Discover web root and set for all handlers to help with paths resolution.
$webroot = (new Webroot($this->config))->discover() ?: Webroot::WEB;
if (!is_string($webroot)) {
- throw new \RuntimeException('Webroot could not be discovered.');
+ throw new \RuntimeException('Web root could not be discovered.');
}
foreach ($classes as $class) {
@@ -728,4 +488,75 @@ protected function initHandlers(): void {
}
}
+ /**
+ * Convert handler properties to Laravel prompts.
+ *
+ * Do not optimize this method to ease debugging and future changes.
+ *
+ * @param string $handler_class
+ * The handler class name.
+ * The handler id.
+ * @param mixed $default_override
+ * Optional override for the default value (for response dependencies).
+ * @param array $responses
+ * Current form responses for context-aware methods.
+ *
+ * @return array
+ * Array of prompt arguments suitable for Laravel prompts.
+ */
+ private function args(string $handler_class, mixed $default_override = NULL, array $responses = []): array {
+ $id = $handler_class::id();
+
+ if (!array_key_exists($id, $this->handlers)) {
+ throw new \RuntimeException(sprintf('Handler for "%s" not found.', $id));
+ }
+
+ $handler = $this->handlers[$handler_class::id()];
+
+ $args = [
+ 'label' => $this->label($handler->label()),
+ 'hint' => $handler->hint($responses),
+ 'placeholder' => $handler->placeholder($responses),
+ 'transform' => $handler->transform(),
+ 'validate' => $handler->validate(),
+ ];
+
+ if ($handler->isRequired()) {
+ $args['required'] = TRUE;
+ }
+
+ $options = $handler->options($responses);
+ if (is_array($options) && $options !== []) {
+ $args['options'] = $options;
+ }
+
+ // Find appropriate default value.
+ $default_from_handler = $handler->default($responses);
+
+ $env_var = static::makeEnvName($id);
+ $env_val = Env::get($env_var);
+ $default_from_env = is_null($env_val) ? NULL : Env::toValue($env_val);
+
+ $default_from_discovery = $this->handlers[$id]->discover();
+
+ if (!is_null($default_from_env)) {
+ $default = $default_from_env;
+ }
+ elseif (!is_null($default_from_discovery)) {
+ $default = $default_from_discovery;
+ }
+ elseif (!is_null($default_override)) {
+ $default = $default_override;
+ }
+ else {
+ $default = $default_from_handler;
+ }
+
+ if (!is_null($default) && $default !== '') {
+ $args['default'] = $default;
+ }
+
+ return array_filter($args, fn($value): bool => $value !== NULL);
+ }
+
}
diff --git a/.vortex/installer/src/Utils/Env.php b/.vortex/installer/src/Utils/Env.php
index 54b2e3cba..8e5b3e192 100644
--- a/.vortex/installer/src/Utils/Env.php
+++ b/.vortex/installer/src/Utils/Env.php
@@ -18,7 +18,7 @@ class Env {
public static function get(string $name, mixed $default = NULL): mixed {
$vars = getenv();
- return !isset($vars[$name]) || $vars[$name] === '' ? $default : $vars[$name];
+ return $vars[$name] ?? $default;
}
public static function put(string $name, string $value): void {
diff --git a/.vortex/installer/tests/Unit/EnvTest.php b/.vortex/installer/tests/Unit/EnvTest.php
index ae4f8b341..dc1508b50 100644
--- a/.vortex/installer/tests/Unit/EnvTest.php
+++ b/.vortex/installer/tests/Unit/EnvTest.php
@@ -61,7 +61,7 @@ public static function dataProviderGet(): array {
['VAR', 'VAL1', NULL, 'VAL1'],
['VAR', 'VAL1', 'VAL2', 'VAL1'],
['VAR', '', 'DEF1', 'DEF1'],
- ['VAR', '', NULL, NULL],
+ ['VAR', '', NULL, ''],
['VAR', '', 'VAL2', 'VAL2'],
];
}
diff --git a/.vortex/installer/tests/Unit/PromptManagerTest.php b/.vortex/installer/tests/Unit/PromptManagerTest.php
index 3ca08abe7..f935b92b4 100644
--- a/.vortex/installer/tests/Unit/PromptManagerTest.php
+++ b/.vortex/installer/tests/Unit/PromptManagerTest.php
@@ -110,11 +110,11 @@ protected function tearDown(): void {
* Test responses.
*
* @code
- * composer test -- --filter=testPrompt@"name of the data provider"
+ * composer test -- --filter=testRunPrompts@"name of the data provider"
* @endcode
*/
- #[DataProvider('dataProviderPrompt')]
- public function testPrompt(
+ #[DataProvider('dataProviderRunPrompts')]
+ public function testRunPrompts(
array $answers,
array|string $expected,
?callable $before = NULL,
@@ -133,12 +133,12 @@ public function testPrompt(
$before($this, $config);
}
- $answers = array_replace(static::defaultAnswers(), $answers);
+ $answers = array_replace(static::defaultTuiAnswers(), $answers);
$keystrokes = static::tuiKeystrokes($answers, 40);
Prompt::fake($keystrokes);
$pm = new PromptManager($config);
- $pm->prompt();
+ $pm->runPrompts();
if (!$exception) {
$actual = $pm->getResponses();
@@ -146,7 +146,13 @@ public function testPrompt(
}
}
- public static function defaultAnswers(): array {
+ /**
+ * The default answers for TUI prompts used in tests.
+ *
+ * @return array
+ * An associative array of prompt IDs and their default values.
+ */
+ public static function defaultTuiAnswers(): array {
return [
Name::id() => static::TUI_DEFAULT,
MachineName::id() => static::TUI_DEFAULT,
@@ -175,7 +181,8 @@ public static function defaultAnswers(): array {
];
}
- public static function dataProviderPrompt(): array {
+ public static function dataProviderRunPrompts(): array {
+ // Expected defaults for a new project.
$expected_defaults = [
Name::id() => 'myproject',
MachineName::id() => 'myproject',
@@ -204,6 +211,7 @@ public static function dataProviderPrompt(): array {
AiCodeInstructions::id() => AiCodeInstructions::NONE,
];
+ // Expected values for a pre-installed project.
$expected_installed = [
CiProvider::id() => CiProvider::NONE,
DependencyUpdatesProvider::id() => DependencyUpdatesProvider::NONE,
@@ -213,7 +221,8 @@ public static function dataProviderPrompt(): array {
PreserveDocsOnboarding::id() => FALSE,
] + $expected_defaults;
- $expected_discovered = [
+ // Expected values for a custom project. Used for testing discovery.
+ $expected_custom = [
Name::id() => 'Discovered project',
MachineName::id() => 'discovered_project',
Org::id() => 'Discovered project Org',
@@ -232,9 +241,9 @@ public static function dataProviderPrompt(): array {
'project name - discovery' => [
[],
- $expected_discovered,
+ $expected_custom,
function (PromptManagerTest $test): void {
- $test->stubComposerJsonValue('description', 'Drupal 10 Standard installation of Discovered project for Discovered project Org');
+ $test->stubComposerJsonValue('description', 'Drupal 11 Standard installation of Discovered project for Discovered project Org');
},
],
'invalid project name' => [
@@ -248,7 +257,7 @@ function (PromptManagerTest $test): void {
Name::id() => 'myproject',
MachineName::id() => 'discovered_project',
Org::id() => 'myproject Org',
- ] + $expected_discovered,
+ ] + $expected_custom,
function (PromptManagerTest $test): void {
$test->stubComposerJsonValue('name', 'discovered_project_org/discovered_project');
},
@@ -260,7 +269,7 @@ function (PromptManagerTest $test): void {
MachineName::id() => 'discovered-project',
Org::id() => 'myproject Org',
GithubRepo::id() => 'discovered_project_org/discovered-project',
- ] + $expected_discovered,
+ ] + $expected_custom,
function (PromptManagerTest $test): void {
$test->stubComposerJsonValue('name', 'discovered_project_org/discovered-project');
},
@@ -272,9 +281,9 @@ function (PromptManagerTest $test): void {
'org name - discovery' => [
[],
- $expected_discovered,
+ $expected_custom,
function (PromptManagerTest $test): void {
- $test->stubComposerJsonValue('description', 'Drupal 10 Standard installation of Discovered project for Discovered project Org');
+ $test->stubComposerJsonValue('description', 'Drupal 11 Standard installation of Discovered project for Discovered project Org');
},
],
'org name - invalid' => [
@@ -288,7 +297,7 @@ function (PromptManagerTest $test): void {
Name::id() => 'myproject',
MachineName::id() => 'discovered_project',
Org::id() => 'myproject Org',
- ] + $expected_discovered,
+ ] + $expected_custom,
function (PromptManagerTest $test): void {
$test->stubComposerJsonValue('name', 'discovered_project_org/discovered_project');
},
@@ -301,7 +310,7 @@ function (PromptManagerTest $test): void {
Org::id() => 'myproject Org',
OrgMachineName::id() => 'discovered-project-org',
GithubRepo::id() => 'discovered-project-org/discovered_project',
- ] + $expected_discovered,
+ ] + $expected_custom,
function (PromptManagerTest $test): void {
$test->stubComposerJsonValue('name', 'discovered-project-org/discovered_project');
},
@@ -415,7 +424,6 @@ function (PromptManagerTest $test, Config $config): void {
[Profile::id() => Key::DOWN . Key::ENTER],
[Profile::id() => 'minimal'] + $expected_defaults,
],
-
'profile - custom' => [
[Profile::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER . 'myprofile'],
[Profile::id() => 'myprofile'] + $expected_defaults,
@@ -647,7 +655,6 @@ function (PromptManagerTest $test, Config $config): void {
'database image - discovery' => [
[
- GithubRepo::id() => static::TUI_SKIP,
DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER,
],
[
@@ -660,7 +667,6 @@ function (PromptManagerTest $test, Config $config): void {
],
'database image - valid' => [
[
- GithubRepo::id() => static::TUI_SKIP,
DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER,
DatabaseImage::id() => 'myregistry/myimage:mytag',
],
@@ -668,7 +674,6 @@ function (PromptManagerTest $test, Config $config): void {
],
'database image - invalid' => [
[
- GithubRepo::id() => static::TUI_SKIP,
DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER,
DatabaseImage::id() => 'myregistry:myimage:mytag',
],
@@ -676,7 +681,6 @@ function (PromptManagerTest $test, Config $config): void {
],
'database image - invalid - capitalization' => [
[
- GithubRepo::id() => static::TUI_SKIP,
DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER,
DatabaseImage::id() => 'MyRegistry/MyImage:mytag',
],