diff --git a/composer.json b/composer.json
index b49eff4c4..9479abfbb 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,11 @@
"role": "Maintainer"
}
],
+ "repositories": [{
+ "type": "path",
+ "url": "../coding-standards-php"
+ }
+ ],
"minimum-stability": "dev",
"require": {
"php": "^8.1",
@@ -53,7 +58,7 @@
"zumba/amplitude-php": "^1.0.4"
},
"require-dev": {
- "acquia/coding-standards": "^2",
+ "acquia/coding-standards": "*",
"brianium/paratest": "^6.6",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
"dominikb/composer-license-checker": "^2.4",
diff --git a/composer.lock b/composer.lock
index 39c1d97dd..36c7ab369 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ab4ff60bae8f6336db700b8e95faf26f",
+ "content-hash": "af855bbe270547764bcca9fce1344172",
"packages": [
{
"name": "acquia/drupal-environment-detector",
@@ -6501,17 +6501,11 @@
"packages-dev": [
{
"name": "acquia/coding-standards",
- "version": "v2.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/acquia/coding-standards-php.git",
- "reference": "bb52b780a00ca7586e8b2e502e09dc53c5e90a60"
- },
+ "version": "3.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/acquia/coding-standards-php/zipball/bb52b780a00ca7586e8b2e502e09dc53c5e90a60",
- "reference": "bb52b780a00ca7586e8b2e502e09dc53c5e90a60",
- "shasum": ""
+ "type": "path",
+ "url": "../coding-standards-php",
+ "reference": "b4bef3bc0af6fcbfcd087a5a194e55116709932e"
},
"require": {
"drupal/coder": "^8.3",
@@ -6534,9 +6528,8 @@
"Acquia\\CodingStandards\\": "src/"
}
},
- "notification-url": "https://packagist.org/downloads/",
"license": [
- "GPL-2.0-only"
+ "GPL-2.0-or-later"
],
"authors": [
{
@@ -6547,7 +6540,7 @@
],
"description": "PHP_CodeSniffer rules (sniffs) for Acquia coding standards",
"keywords": [
- "drupal",
+ "Drupal",
"phpcs",
"standards",
"static analysis"
@@ -6556,7 +6549,9 @@
"issues": "https://github.com/acquia/coding-standards/issues",
"source": "https://github.com/acquia/coding-standards"
},
- "time": "2023-03-06T17:49:20+00:00"
+ "transport-options": {
+ "relative": true
+ }
},
{
"name": "amphp/amp",
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index d80a25759..9244f7f39 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -22,37 +22,7 @@
tests/fixtures/*
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -62,4 +32,5 @@
+
diff --git a/src/AcsfApi/AcsfClient.php b/src/AcsfApi/AcsfClient.php
index afeccd501..63a94eeb0 100644
--- a/src/AcsfApi/AcsfClient.php
+++ b/src/AcsfApi/AcsfClient.php
@@ -1,6 +1,6 @@
getBody();
- $body = json_decode((string) $bodyJson, FALSE, 512, JSON_THROW_ON_ERROR);
-
- // ACSF sometimes returns an array rather than an object.
- if (is_array($body)) {
- return $body;
+class AcsfClient extends Client
+{
+ public function processResponse(ResponseInterface $response): mixed
+ {
+ $bodyJson = $response->getBody();
+ $body = json_decode((string) $bodyJson, false, 512, JSON_THROW_ON_ERROR);
+
+ // ACSF sometimes returns an array rather than an object.
+ if (is_array($body)) {
+ return $body;
+ }
+
+ if (property_exists($body, '_embedded') && property_exists($body->_embedded, 'items')) {
+ return $body->_embedded->items;
+ }
+
+ if (property_exists($body, 'error') && property_exists($body, 'message')) {
+ throw new ApiErrorException($body);
+ }
+ // Throw error for 4xx and 5xx responses.
+ if (property_exists($body, 'message') && in_array(substr((string) $response->getStatusCode(), 0, 1), ['4', '5'], true)) {
+ $body->error = $response->getStatusCode();
+ throw new ApiErrorException($body);
+ }
+
+ return $body;
}
-
- if (property_exists($body, '_embedded') && property_exists($body->_embedded, 'items')) {
- return $body->_embedded->items;
- }
-
- if (property_exists($body, 'error') && property_exists($body, 'message')) {
- throw new ApiErrorException($body);
- }
- // Throw error for 4xx and 5xx responses.
- if (property_exists($body, 'message') && in_array(substr((string) $response->getStatusCode(), 0, 1), ['4', '5'], TRUE)) {
- $body->error = $response->getStatusCode();
- throw new ApiErrorException($body);
- }
-
- return $body;
- }
-
}
diff --git a/src/AcsfApi/AcsfClientService.php b/src/AcsfApi/AcsfClientService.php
index 78ecfc587..ae78602cd 100644
--- a/src/AcsfApi/AcsfClientService.php
+++ b/src/AcsfApi/AcsfClientService.php
@@ -1,27 +1,29 @@
connector);
- $this->configureClient($client);
-
- return $client;
- }
-
- protected function checkAuthentication(): bool {
- return ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret());
- }
-
+class AcsfClientService extends ClientService
+{
+ public function __construct(AcsfConnectorFactory $connectorFactory, Application $application, AcsfCredentials $cloudCredentials)
+ {
+ parent::__construct($connectorFactory, $application, $cloudCredentials);
+ }
+
+ public function getClient(): AcsfClient
+ {
+ $client = AcsfClient::factory($this->connector);
+ $this->configureClient($client);
+
+ return $client;
+ }
+
+ protected function checkAuthentication(): bool
+ {
+ return ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret());
+ }
}
diff --git a/src/AcsfApi/AcsfConnector.php b/src/AcsfApi/AcsfConnector.php
index 5c6d0c80d..24dc8778e 100644
--- a/src/AcsfApi/AcsfConnector.php
+++ b/src/AcsfApi/AcsfConnector.php
@@ -1,6 +1,6 @@
$config
- * @param string|null $baseUri
- * @param string|null $urlAccessToken
- */
- public function __construct(array $config, string $baseUri = NULL, string $urlAccessToken = NULL) {
- parent::__construct($config, $baseUri, $urlAccessToken);
-
- $this->client = new GuzzleClient([
- 'auth' => [
+class AcsfConnector extends Connector
+{
+ /**
+ * @param array $config
+ * @param string|null $baseUri
+ * @param string|null $urlAccessToken
+ */
+ public function __construct(array $config, string $baseUri = null, string $urlAccessToken = null)
+ {
+ parent::__construct($config, $baseUri, $urlAccessToken);
+
+ $this->client = new GuzzleClient([
+ 'auth' => [
$config['key'],
$config['secret'],
- ],
- 'base_uri' => $this->getBaseUri(),
- ]);
- }
-
- /**
- * @param array $options
- */
- public function sendRequest(string $verb, string $path, array $options): ResponseInterface {
- return $this->client->request($verb, $path, $options);
- }
-
+ ],
+ 'base_uri' => $this->getBaseUri(),
+ ]);
+ }
+
+ /**
+ * @param array $options
+ */
+ public function sendRequest(string $verb, string $path, array $options): ResponseInterface
+ {
+ return $this->client->request($verb, $path, $options);
+ }
}
diff --git a/src/AcsfApi/AcsfConnectorFactory.php b/src/AcsfApi/AcsfConnectorFactory.php
index 4a1228fa9..186ec5f20 100644
--- a/src/AcsfApi/AcsfConnectorFactory.php
+++ b/src/AcsfApi/AcsfConnectorFactory.php
@@ -1,21 +1,22 @@
$config
- */
- public function __construct(protected array $config, protected ?string $baseUri = NULL) {
- }
-
- public function createConnector(): AcsfConnector {
- return new AcsfConnector($this->config, $this->baseUri);
- }
+class AcsfConnectorFactory implements ConnectorFactoryInterface
+{
+ /**
+ * @param array $config
+ */
+ public function __construct(protected array $config, protected ?string $baseUri = null)
+ {
+ }
+ public function createConnector(): AcsfConnector
+ {
+ return new AcsfConnector($this->config, $this->baseUri);
+ }
}
diff --git a/src/AcsfApi/AcsfCredentials.php b/src/AcsfApi/AcsfCredentials.php
index 6fa966ddc..971e98bdf 100644
--- a/src/AcsfApi/AcsfCredentials.php
+++ b/src/AcsfApi/AcsfCredentials.php
@@ -1,74 +1,79 @@
getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) {
- return $activeUser['username'];
- }
+ public function getCloudKey(): ?string
+ {
+ if (getenv('ACSF_USERNAME')) {
+ return getenv('ACSF_USERNAME');
+ }
- return NULL;
- }
+ if (($currentFactory = $this->getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) {
+ return $activeUser['username'];
+ }
- /**
- * @param array $factory
- */
- public function getFactoryActiveUser(array $factory): mixed {
- if (array_key_exists('active_user', $factory)) {
- $activeUser = $factory['active_user'];
- if (array_key_exists($activeUser, $factory['users'])) {
- return $factory['users'][$activeUser];
- }
+ return null;
}
- return NULL;
- }
-
- private function getCurrentFactory(): mixed {
- if (($factory = $this->datastoreCloud->get('acsf_active_factory')) && ($acsfFactories = $this->datastoreCloud->get('acsf_factories')) && array_key_exists($factory, $acsfFactories)) {
- return $acsfFactories[$factory];
+ /**
+ * @param array $factory
+ */
+ public function getFactoryActiveUser(array $factory): mixed
+ {
+ if (array_key_exists('active_user', $factory)) {
+ $activeUser = $factory['active_user'];
+ if (array_key_exists($activeUser, $factory['users'])) {
+ return $factory['users'][$activeUser];
+ }
+ }
+
+ return null;
}
- return NULL;
- }
- public function getCloudSecret(): ?string {
- if (getenv('ACSF_KEY')) {
- return getenv('ACSF_KEY');
+ private function getCurrentFactory(): mixed
+ {
+ if (($factory = $this->datastoreCloud->get('acsf_active_factory')) && ($acsfFactories = $this->datastoreCloud->get('acsf_factories')) && array_key_exists($factory, $acsfFactories)) {
+ return $acsfFactories[$factory];
+ }
+ return null;
}
- if (($currentFactory = $this->getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) {
- return $activeUser['key'];
- }
+ public function getCloudSecret(): ?string
+ {
+ if (getenv('ACSF_KEY')) {
+ return getenv('ACSF_KEY');
+ }
- return NULL;
- }
+ if (($currentFactory = $this->getCurrentFactory()) && $activeUser = $this->getFactoryActiveUser($currentFactory)) {
+ return $activeUser['key'];
+ }
- public function getBaseUri(): ?string {
- if (getenv('ACSF_FACTORY_URI')) {
- return getenv('ACSF_FACTORY_URI');
- }
- if ($factory = $this->datastoreCloud->get('acsf_active_factory')) {
- return $factory;
+ return null;
}
- return NULL;
- }
+ public function getBaseUri(): ?string
+ {
+ if (getenv('ACSF_FACTORY_URI')) {
+ return getenv('ACSF_FACTORY_URI');
+ }
+ if ($factory = $this->datastoreCloud->get('acsf_active_factory')) {
+ return $factory;
+ }
+ return null;
+ }
}
diff --git a/src/ApiCredentialsInterface.php b/src/ApiCredentialsInterface.php
index bddeb5caf..1224e1db0 100644
--- a/src/ApiCredentialsInterface.php
+++ b/src/ApiCredentialsInterface.php
@@ -1,15 +1,14 @@
helpMessages;
- }
-
- public function setHelpMessages(array $helpMessages): void {
- $this->helpMessages = $helpMessages;
- }
-
- public function renderThrowable(
- Throwable $e,
- OutputInterface $output
- ): void {
- parent::renderThrowable($e, $output);
-
- if ($this->getHelpMessages()) {
- $io = new SymfonyStyle(new ArrayInput([]), $output);
- $outputStyle = new OutputFormatterStyle('white', 'blue');
- $output->getFormatter()->setStyle('help', $outputStyle);
- $io->block($this->getHelpMessages(), 'help', 'help', ' ', TRUE, FALSE);
+class Application extends \Symfony\Component\Console\Application
+{
+ /**
+ * @var string[]
+ */
+ protected array $helpMessages = [];
+
+ /**
+ * @return string[]
+ */
+ private function getHelpMessages(): array
+ {
+ return $this->helpMessages;
}
- }
+ public function setHelpMessages(array $helpMessages): void
+ {
+ $this->helpMessages = $helpMessages;
+ }
+
+ public function renderThrowable(
+ Throwable $e,
+ OutputInterface $output
+ ): void {
+ parent::renderThrowable($e, $output);
+
+ if ($this->getHelpMessages()) {
+ $io = new SymfonyStyle(new ArrayInput([]), $output);
+ $outputStyle = new OutputFormatterStyle('white', 'blue');
+ $output->getFormatter()->setStyle('help', $outputStyle);
+ $io->block($this->getHelpMessages(), 'help', 'help', ' ', true, false);
+ }
+ }
}
diff --git a/src/Attribute/RequireAuth.php b/src/Attribute/RequireAuth.php
index af6ca0c80..e28cc8d47 100644
--- a/src/Attribute/RequireAuth.php
+++ b/src/Attribute/RequireAuth.php
@@ -1,6 +1,6 @@
$config
- */
- public function __construct(array $config, string $baseUri = NULL, string $urlAccessToken = NULL) {
- $this->accessToken = new AccessToken(['access_token' => $config['access_token']]);
- parent::__construct($config, $baseUri, $urlAccessToken);
- }
-
- public function createRequest(string $verb, string $path): RequestInterface {
- if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) {
- if (!file_exists($file)) {
- throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]);
- }
- $this->accessToken = new AccessToken(['access_token' => trim(file_get_contents($file), "\"\n")]);
+class AccessTokenConnector extends Connector
+{
+ /**
+ * @var \League\OAuth2\Client\Provider\GenericProvider
+ */
+ protected AbstractProvider $provider;
+
+ /**
+ * @param array $config
+ */
+ public function __construct(array $config, string $baseUri = null, string $urlAccessToken = null)
+ {
+ $this->accessToken = new AccessToken(['access_token' => $config['access_token']]);
+ parent::__construct($config, $baseUri, $urlAccessToken);
}
- return $this->provider->getAuthenticatedRequest(
- $verb,
- $this->getBaseUri() . $path,
- $this->accessToken
- );
- }
-
- public function setProvider(
- GenericProvider $provider
- ): void {
- $this->provider = $provider;
- }
-
- public function getAccessToken(): AccessToken {
- return $this->accessToken;
- }
+ public function createRequest(string $verb, string $path): RequestInterface
+ {
+ if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) {
+ if (!file_exists($file)) {
+ throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]);
+ }
+ $this->accessToken = new AccessToken(['access_token' => trim(file_get_contents($file), "\"\n")]);
+ }
+ return $this->provider->getAuthenticatedRequest(
+ $verb,
+ $this->getBaseUri() . $path,
+ $this->accessToken
+ );
+ }
+
+ public function setProvider(
+ GenericProvider $provider
+ ): void {
+ $this->provider = $provider;
+ }
+
+ public function getAccessToken(): AccessToken
+ {
+ return $this->accessToken;
+ }
}
diff --git a/src/CloudApi/ClientService.php b/src/CloudApi/ClientService.php
index 96afa2ad8..95acecea6 100644
--- a/src/CloudApi/ClientService.php
+++ b/src/CloudApi/ClientService.php
@@ -1,6 +1,6 @@
connectorFactory = $connectorFactory;
- $this->setConnector($connectorFactory->createConnector());
- $this->setApplication($application);
- }
-
- public function setConnector(ConnectorInterface $connector): void {
- $this->connector = $connector;
- }
+ public function __construct(ConnectorFactoryInterface $connectorFactory, Application $application, protected ApiCredentialsInterface $credentials)
+ {
+ $this->connectorFactory = $connectorFactory;
+ $this->setConnector($connectorFactory->createConnector());
+ $this->setApplication($application);
+ }
- private function setApplication(Application $application): void {
- $this->application = $application;
- }
+ public function setConnector(ConnectorInterface $connector): void
+ {
+ $this->connector = $connector;
+ }
- public function getClient(): Client {
- $client = Client::factory($this->connector);
- $this->configureClient($client);
+ private function setApplication(Application $application): void
+ {
+ $this->application = $application;
+ }
- return $client;
- }
+ public function getClient(): Client
+ {
+ $client = Client::factory($this->connector);
+ $this->configureClient($client);
- protected function configureClient(Client $client): void {
- $userAgent = sprintf("acli/%s", $this->application->getVersion());
- $customHeaders = [
- 'User-Agent' => [$userAgent],
- ];
- if ($uuid = getenv("REMOTEIDE_UUID")) {
- $customHeaders['X-Cloud-IDE-UUID'] = $uuid;
+ return $client;
}
- $client->addOption('headers', $customHeaders);
- }
- public function isMachineAuthenticated(): bool {
- if ($this->machineIsAuthenticated !== NULL) {
- return $this->machineIsAuthenticated;
+ protected function configureClient(Client $client): void
+ {
+ $userAgent = sprintf("acli/%s", $this->application->getVersion());
+ $customHeaders = [
+ 'User-Agent' => [$userAgent],
+ ];
+ if ($uuid = getenv("REMOTEIDE_UUID")) {
+ $customHeaders['X-Cloud-IDE-UUID'] = $uuid;
+ }
+ $client->addOption('headers', $customHeaders);
}
- $this->machineIsAuthenticated = $this->checkAuthentication();
- return $this->machineIsAuthenticated;
- }
- protected function checkAuthentication(): bool {
- return (
- $this->credentials->getCloudAccessToken() ||
- ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret())
- );
- }
+ public function isMachineAuthenticated(): bool
+ {
+ if ($this->machineIsAuthenticated !== null) {
+ return $this->machineIsAuthenticated;
+ }
+ $this->machineIsAuthenticated = $this->checkAuthentication();
+ return $this->machineIsAuthenticated;
+ }
+ protected function checkAuthentication(): bool
+ {
+ return (
+ $this->credentials->getCloudAccessToken() ||
+ ($this->credentials->getCloudKey() && $this->credentials->getCloudSecret())
+ );
+ }
}
diff --git a/src/CloudApi/CloudCredentials.php b/src/CloudApi/CloudCredentials.php
index 81b89593d..98ca77847 100644
--- a/src/CloudApi/CloudCredentials.php
+++ b/src/CloudApi/CloudCredentials.php
@@ -1,6 +1,6 @@
$file]);
- }
- return trim(file_get_contents($file), "\"\n");
- }
+ public function getCloudAccessToken(): ?string
+ {
+ if ($token = getenv('ACLI_ACCESS_TOKEN')) {
+ return $token;
+ }
- return NULL;
- }
+ if ($file = getenv('ACLI_ACCESS_TOKEN_FILE')) {
+ if (!file_exists($file)) {
+ throw new AcquiaCliException('Access token file not found at {file}', ['file' => $file]);
+ }
+ return trim(file_get_contents($file), "\"\n");
+ }
- public function getCloudAccessTokenExpiry(): ?string {
- if ($token = getenv('ACLI_ACCESS_TOKEN_EXPIRY')) {
- return $token;
+ return null;
}
- if ($file = getenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE')) {
- if (!file_exists($file)) {
- throw new AcquiaCliException('Access token expiry file not found at {file}', ['file' => $file]);
- }
- return trim(file_get_contents($file), "\"\n");
- }
+ public function getCloudAccessTokenExpiry(): ?string
+ {
+ if ($token = getenv('ACLI_ACCESS_TOKEN_EXPIRY')) {
+ return $token;
+ }
- return NULL;
- }
+ if ($file = getenv('ACLI_ACCESS_TOKEN_EXPIRY_FILE')) {
+ if (!file_exists($file)) {
+ throw new AcquiaCliException('Access token expiry file not found at {file}', ['file' => $file]);
+ }
+ return trim(file_get_contents($file), "\"\n");
+ }
- public function getCloudKey(): ?string {
- if ($key = getenv('ACLI_KEY')) {
- return $key;
+ return null;
}
- if ($this->datastoreCloud->get('acli_key')) {
- return $this->datastoreCloud->get('acli_key');
- }
+ public function getCloudKey(): ?string
+ {
+ if ($key = getenv('ACLI_KEY')) {
+ return $key;
+ }
- return NULL;
- }
+ if ($this->datastoreCloud->get('acli_key')) {
+ return $this->datastoreCloud->get('acli_key');
+ }
- public function getCloudSecret(): ?string {
- if ($secret = getenv('ACLI_SECRET')) {
- return $secret;
+ return null;
}
- $acliKey = $this->getCloudKey();
- if ($this->datastoreCloud->get('keys')) {
- $keys = $this->datastoreCloud->get('keys');
- if (is_array($keys) && array_key_exists($acliKey, $keys)) {
- return $this->datastoreCloud->get('keys')[$acliKey]['secret'];
- }
+ public function getCloudSecret(): ?string
+ {
+ if ($secret = getenv('ACLI_SECRET')) {
+ return $secret;
+ }
+
+ $acliKey = $this->getCloudKey();
+ if ($this->datastoreCloud->get('keys')) {
+ $keys = $this->datastoreCloud->get('keys');
+ if (is_array($keys) && array_key_exists($acliKey, $keys)) {
+ return $this->datastoreCloud->get('keys')[$acliKey]['secret'];
+ }
+ }
+
+ return null;
}
- return NULL;
- }
-
- public function getBaseUri(): ?string {
- if ($uri = getenv('ACLI_CLOUD_API_BASE_URI')) {
- return $uri;
+ public function getBaseUri(): ?string
+ {
+ if ($uri = getenv('ACLI_CLOUD_API_BASE_URI')) {
+ return $uri;
+ }
+ return null;
}
- return NULL;
- }
- public function getAccountsUri(): ?string {
- if ($uri = getenv('ACLI_CLOUD_API_ACCOUNTS_URI')) {
- return $uri;
+ public function getAccountsUri(): ?string
+ {
+ if ($uri = getenv('ACLI_CLOUD_API_ACCOUNTS_URI')) {
+ return $uri;
+ }
+ return null;
}
- return NULL;
- }
-
}
diff --git a/src/CloudApi/ConnectorFactory.php b/src/CloudApi/ConnectorFactory.php
index d42ebce69..f8fd82488 100644
--- a/src/CloudApi/ConnectorFactory.php
+++ b/src/CloudApi/ConnectorFactory.php
@@ -1,6 +1,6 @@
$config
- */
- public function __construct(protected array $config, protected ?string $baseUri = NULL, protected ?string $accountsUri = NULL) {
- }
-
- /**
- * @return \Acquia\Cli\CloudApi\AccessTokenConnector|\AcquiaCloudApi\Connector\Connector
- */
- public function createConnector(): Connector|AccessTokenConnector {
- // A defined key & secret takes priority.
- if ($this->config['key'] && $this->config['secret']) {
- return new Connector($this->config, $this->baseUri, $this->accountsUri);
+class ConnectorFactory implements ConnectorFactoryInterface
+{
+ /**
+ * @param array $config
+ */
+ public function __construct(protected array $config, protected ?string $baseUri = null, protected ?string $accountsUri = null)
+ {
}
- // Fall back to a valid access token.
- if ($this->config['accessToken']) {
- $accessToken = $this->createAccessToken();
- if (!$accessToken->hasExpired()) {
- // @todo Add debug log entry indicating that access token is being used.
- return new AccessTokenConnector([
- 'access_token' => $accessToken,
- 'key' => NULL,
- 'secret' => NULL,
- ], $this->baseUri, $this->accountsUri);
- }
+ /**
+ * @return \Acquia\Cli\CloudApi\AccessTokenConnector|\AcquiaCloudApi\Connector\Connector
+ */
+ public function createConnector(): Connector|AccessTokenConnector
+ {
+ // A defined key & secret takes priority.
+ if ($this->config['key'] && $this->config['secret']) {
+ return new Connector($this->config, $this->baseUri, $this->accountsUri);
+ }
+
+ // Fall back to a valid access token.
+ if ($this->config['accessToken']) {
+ $accessToken = $this->createAccessToken();
+ if (!$accessToken->hasExpired()) {
+ // @todo Add debug log entry indicating that access token is being used.
+ return new AccessTokenConnector([
+ 'access_token' => $accessToken,
+ 'key' => null,
+ 'secret' => null,
+ ], $this->baseUri, $this->accountsUri);
+ }
+ }
+
+ // Fall back to an unauthenticated request.
+ return new Connector($this->config, $this->baseUri, $this->accountsUri);
}
- // Fall back to an unauthenticated request.
- return new Connector($this->config, $this->baseUri, $this->accountsUri);
- }
-
- private function createAccessToken(): AccessToken {
- return new AccessToken([
- 'access_token' => $this->config['accessToken'],
- 'expires' => $this->config['accessTokenExpiry'],
- ]);
- }
-
+ private function createAccessToken(): AccessToken
+ {
+ return new AccessToken([
+ 'access_token' => $this->config['accessToken'],
+ 'expires' => $this->config['accessTokenExpiry'],
+ ]);
+ }
}
diff --git a/src/Command/Acsf/AcsfCommandFactory.php b/src/Command/Acsf/AcsfCommandFactory.php
index 510f40d56..9031d4590 100644
--- a/src/Command/Acsf/AcsfCommandFactory.php
+++ b/src/Command/Acsf/AcsfCommandFactory.php
@@ -1,6 +1,6 @@
localMachineHelper,
- $this->datastoreCloud,
- $this->datastoreAcli,
- $this->cloudCredentials,
- $this->telemetryHelper,
- $this->projectDir,
- $this->cloudApiClientService,
- $this->sshHelper,
- $this->sshDir,
- $this->logger,
- );
- }
-
- public function createListCommand(): AcsfListCommand {
- return new AcsfListCommand(
- $this->localMachineHelper,
- $this->datastoreCloud,
- $this->datastoreAcli,
- $this->cloudCredentials,
- $this->telemetryHelper,
- $this->projectDir,
- $this->cloudApiClientService,
- $this->sshHelper,
- $this->sshDir,
- $this->logger,
- );
- }
+ public function createCommand(): ApiBaseCommand
+ {
+ return new ApiBaseCommand(
+ $this->localMachineHelper,
+ $this->datastoreCloud,
+ $this->datastoreAcli,
+ $this->cloudCredentials,
+ $this->telemetryHelper,
+ $this->projectDir,
+ $this->cloudApiClientService,
+ $this->sshHelper,
+ $this->sshDir,
+ $this->logger,
+ );
+ }
+ public function createListCommand(): AcsfListCommand
+ {
+ return new AcsfListCommand(
+ $this->localMachineHelper,
+ $this->datastoreCloud,
+ $this->datastoreAcli,
+ $this->cloudCredentials,
+ $this->telemetryHelper,
+ $this->projectDir,
+ $this->cloudApiClientService,
+ $this->sshHelper,
+ $this->sshDir,
+ $this->logger,
+ );
+ }
}
diff --git a/src/Command/Acsf/AcsfListCommand.php b/src/Command/Acsf/AcsfListCommand.php
index 7262d17f8..69cc91379 100644
--- a/src/Command/Acsf/AcsfListCommand.php
+++ b/src/Command/Acsf/AcsfListCommand.php
@@ -1,6 +1,6 @@
namespace = $namespace;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $commands = $this->getApplication()->all();
- foreach ($commands as $command) {
- if ($command->getName() !== $this->namespace
- // E.g., if the namespace is acsf:api, show all acsf:api:* commands.
- && str_contains($command->getName(), $this->namespace . ':')
- // This is a lazy way to exclude api:base and acsf:base.
- && $command->getDescription()
- ) {
- $command->setHidden(FALSE);
- }
- else {
- $command->setHidden();
- }
- }
-
- $command = $this->getApplication()->find('list');
- $arguments = [
- 'command' => 'list',
- 'namespace' => 'acsf',
- ];
- $listInput = new ArrayInput($arguments);
+class AcsfListCommandBase extends CommandBase
+{
+ protected string $namespace;
- return $command->run($listInput, $output);
- }
+ public function setNamespace(string $namespace): void
+ {
+ $this->namespace = $namespace;
+ }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $commands = $this->getApplication()->all();
+ foreach ($commands as $command) {
+ if (
+ $command->getName() !== $this->namespace
+ // E.g., if the namespace is acsf:api, show all acsf:api:* commands.
+ && str_contains($command->getName(), $this->namespace . ':')
+ // This is a lazy way to exclude api:base and acsf:base.
+ && $command->getDescription()
+ ) {
+ $command->setHidden(false);
+ } else {
+ $command->setHidden();
+ }
+ }
+
+ $command = $this->getApplication()->find('list');
+ $arguments = [
+ 'command' => 'list',
+ 'namespace' => 'acsf',
+ ];
+ $listInput = new ArrayInput($arguments);
+
+ return $command->run($listInput, $output);
+ }
}
diff --git a/src/Command/Api/ApiBaseCommand.php b/src/Command/Api/ApiBaseCommand.php
index 1a9a743e6..6f6bc3545 100644
--- a/src/Command/Api/ApiBaseCommand.php
+++ b/src/Command/Api/ApiBaseCommand.php
@@ -1,6 +1,6 @@
- */
- protected array $responses;
-
- /**
- * @var array
- */
- protected array $servers;
-
- protected string $path;
-
- /**
- * @var array
- */
- private array $queryParams = [];
-
- /**
- * @var array
- */
- private array $postParams = [];
-
- /**
- * @var array
- */
- private array $pathParams = [];
-
- protected function interact(InputInterface $input, OutputInterface $output): void {
- $params = array_merge($this->queryParams, $this->postParams, $this->pathParams);
- foreach ($this->getDefinition()->getArguments() as $argument) {
- if ($argument->isRequired() && !$input->getArgument($argument->getName())) {
- $this->io->note([
- "{$argument->getName()} is a required argument.",
- $argument->getDescription(),
- ]);
- // Choice question.
- if (array_key_exists($argument->getName(), $params)
- && array_key_exists('schema', $params[$argument->getName()])
- && array_key_exists('enum', $params[$argument->getName()]['schema'])) {
- $choices = $params[$argument->getName()]['schema']['enum'];
- $answer = $this->io->choice("Select a value for {$argument->getName()}", $choices, $argument->getDefault());
+#[AsCommand(name: 'api:base', hidden: true)]
+class ApiBaseCommand extends CommandBase
+{
+ protected string $method;
+
+ /**
+ * @var array
+ */
+ protected array $responses;
+
+ /**
+ * @var array
+ */
+ protected array $servers;
+
+ protected string $path;
+
+ /**
+ * @var array
+ */
+ private array $queryParams = [];
+
+ /**
+ * @var array
+ */
+ private array $postParams = [];
+
+ /**
+ * @var array
+ */
+ private array $pathParams = [];
+
+ protected function interact(InputInterface $input, OutputInterface $output): void
+ {
+ $params = array_merge($this->queryParams, $this->postParams, $this->pathParams);
+ foreach ($this->getDefinition()->getArguments() as $argument) {
+ if ($argument->isRequired() && !$input->getArgument($argument->getName())) {
+ $this->io->note([
+ "{$argument->getName()} is a required argument.",
+ $argument->getDescription(),
+ ]);
+ // Choice question.
+ if (
+ array_key_exists($argument->getName(), $params)
+ && array_key_exists('schema', $params[$argument->getName()])
+ && array_key_exists('enum', $params[$argument->getName()]['schema'])
+ ) {
+ $choices = $params[$argument->getName()]['schema']['enum'];
+ $answer = $this->io->choice("Select a value for {$argument->getName()}", $choices, $argument->getDefault());
+ } elseif (
+ array_key_exists($argument->getName(), $params)
+ && array_key_exists('type', $params[$argument->getName()])
+ && $params[$argument->getName()]['type'] === 'boolean'
+ ) {
+ $answer = $this->io->choice("Select a value for {$argument->getName()}", ['false', 'true'], $argument->getDefault());
+ $answer = $answer === 'true';
+ }
+ // Free form.
+ else {
+ $answer = $this->askFreeFormQuestion($argument, $params);
+ }
+ $input->setArgument($argument->getName(), $answer);
+ }
}
- elseif (array_key_exists($argument->getName(), $params)
- && array_key_exists('type', $params[$argument->getName()])
- && $params[$argument->getName()]['type'] === 'boolean') {
- $answer = $this->io->choice("Select a value for {$argument->getName()}", ['false', 'true'], $argument->getDefault());
- $answer = $answer === 'true';
+ parent::interact($input, $output);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if ($this->getName() === 'api:base') {
+ throw new AcquiaCliException('api:base is not a valid command');
}
- // Free form.
- else {
- $answer = $this->askFreeFormQuestion($argument, $params);
+ // Build query from non-null options.
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $this->addQueryParamsToClient($input, $acquiaCloudClient);
+ $this->addPostParamsToClient($input, $acquiaCloudClient);
+ // Acquia PHP SDK cannot set the Accept header itself because it would break
+ // API calls returning octet streams (e.g., db backups). It's safe to use
+ // here because the API command should always return JSON.
+ $acquiaCloudClient->addOption('headers', [
+ 'Accept' => 'application/hal+json, version=2',
+ ]);
+
+ try {
+ if ($this->output->isVeryVerbose()) {
+ $acquiaCloudClient->addOption('debug', $this->output);
+ }
+ $path = $this->getRequestPath($input);
+ $response = $acquiaCloudClient->request($this->method, $path);
+ $exitCode = 0;
}
- $input->setArgument($argument->getName(), $answer);
- }
- }
- parent::interact($input, $output);
- }
+ // Ignore PhpStorm warning here.
+ // @see https://youtrack.jetbrains.com/issue/WI-77190/Exception-is-never-thrown-when-thrown-from-submethod
+ catch (ApiErrorException $exception) {
+ $response = $exception->getResponseBody();
+ $exitCode = 1;
+ }
+
+ $contents = json_encode($response, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
+ $this->output->writeln($contents);
- protected function execute(InputInterface $input, OutputInterface $output): int {
- if ($this->getName() === 'api:base') {
- throw new AcquiaCliException('api:base is not a valid command');
+ return $exitCode;
}
- // Build query from non-null options.
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $this->addQueryParamsToClient($input, $acquiaCloudClient);
- $this->addPostParamsToClient($input, $acquiaCloudClient);
- // Acquia PHP SDK cannot set the Accept header itself because it would break
- // API calls returning octet streams (e.g., db backups). It's safe to use
- // here because the API command should always return JSON.
- $acquiaCloudClient->addOption('headers', [
- 'Accept' => 'application/hal+json, version=2',
- ]);
-
- try {
- if ($this->output->isVeryVerbose()) {
- $acquiaCloudClient->addOption('debug', $this->output);
- }
- $path = $this->getRequestPath($input);
- $response = $acquiaCloudClient->request($this->method, $path);
- $exitCode = 0;
+
+ public function setMethod(string $method): void
+ {
+ $this->method = $method;
}
- // Ignore PhpStorm warning here.
- // @see https://youtrack.jetbrains.com/issue/WI-77190/Exception-is-never-thrown-when-thrown-from-submethod
- catch (ApiErrorException $exception) {
- $response = $exception->getResponseBody();
- $exitCode = 1;
+
+ public function setResponses(array $responses): void
+ {
+ $this->responses = $responses;
}
- $contents = json_encode($response, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
- $this->output->writeln($contents);
+ public function setServers(array $servers): void
+ {
+ $this->servers = $servers;
+ }
- return $exitCode;
- }
+ public function setPath(string $path): void
+ {
+ $this->path = $path;
+ }
- public function setMethod(string $method): void {
- $this->method = $method;
- }
+ protected function getRequestPath(InputInterface $input): string
+ {
+ $path = $this->path;
+
+ $arguments = $input->getArguments();
+ // The command itself is the first argument. Remove it.
+ array_shift($arguments);
+ foreach ($arguments as $key => $value) {
+ $token = '{' . $key . '}';
+ if (str_contains($path, $token)) {
+ $path = str_replace($token, $value, $path);
+ }
+ }
- public function setResponses(array $responses): void {
- $this->responses = $responses;
- }
+ return $path;
+ }
- public function setServers(array $servers): void {
- $this->servers = $servers;
- }
+ public function getMethod(): string
+ {
+ return $this->method;
+ }
- public function setPath(string $path): void {
- $this->path = $path;
- }
+ public function addPostParameter(string $paramName, mixed $value): void
+ {
+ $this->postParams[$paramName] = $value;
+ }
- protected function getRequestPath(InputInterface $input): string {
- $path = $this->path;
+ public function addQueryParameter(string $paramName, mixed $value): void
+ {
+ $this->queryParams[$paramName] = $value;
+ }
- $arguments = $input->getArguments();
- // The command itself is the first argument. Remove it.
- array_shift($arguments);
- foreach ($arguments as $key => $value) {
- $token = '{' . $key . '}';
- if (str_contains($path, $token)) {
- $path = str_replace($token, $value, $path);
- }
+ public function getPath(): string
+ {
+ return $this->path;
}
- return $path;
- }
+ public function addPathParameter(string $paramName, mixed $value): void
+ {
+ $this->pathParams[$paramName] = $value;
+ }
- public function getMethod(): string {
- return $this->method;
- }
+ private function getParamFromInput(InputInterface $input, string $paramName): array|bool|string|int|null
+ {
+ if ($input->hasArgument($paramName)) {
+ return $input->getArgument($paramName);
+ }
- public function addPostParameter(string $paramName, mixed $value): void {
- $this->postParams[$paramName] = $value;
- }
+ if ($input->hasParameterOption('--' . $paramName)) {
+ return $input->getOption($paramName);
+ }
+ return null;
+ }
- public function addQueryParameter(string $paramName, mixed $value): void {
- $this->queryParams[$paramName] = $value;
- }
+ private function castParamType(array $paramSpec, array|string|bool|int $value): array|bool|int|string|object
+ {
+ $oneOf = $this->getParamTypeOneOf($paramSpec);
+ if (isset($oneOf)) {
+ $types = [];
+ foreach ($oneOf as $type) {
+ if ($type['type'] === 'array' && str_contains($value, ',')) {
+ return $this->castParamToArray($type, $value);
+ }
+ $types[] = $type['type'];
+ }
+ if (in_array('integer', $types, true) && ctype_digit($value)) {
+ return $this->doCastParamType('integer', $value);
+ }
+ } elseif ($paramSpec['type'] === 'array') {
+ if (is_array($value) && count($value) === 1) {
+ return $this->castParamToArray($paramSpec, $value[0]);
+ }
+
+ return $this->castParamToArray($paramSpec, $value);
+ }
- public function getPath(): string {
- return $this->path;
- }
+ $type = $this->getParamType($paramSpec);
+ if (!$type) {
+ return $value;
+ }
- public function addPathParameter(string $paramName, mixed $value): void {
- $this->pathParams[$paramName] = $value;
- }
+ return $this->doCastParamType($type, $value);
+ }
- private function getParamFromInput(InputInterface $input, string $paramName): array|bool|string|int|null {
- if ($input->hasArgument($paramName)) {
- return $input->getArgument($paramName);
+ private function doCastParamType(string $type, mixed $value): array|bool|int|string|object
+ {
+ return match ($type) {
+ 'integer' => (int) $value,
+ 'boolean' => $this->castBool($value),
+ 'array' => is_string($value) ? explode(',', $value) : (array) $value,
+ 'string' => (string) $value,
+ 'object' => json_decode($value, false, 512, JSON_THROW_ON_ERROR),
+ };
}
- if ($input->hasParameterOption('--' . $paramName)) {
- return $input->getOption($paramName);
+ public function castBool(mixed $val): bool
+ {
+ return (bool) (is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : $val);
}
- return NULL;
- }
-
- private function castParamType(array $paramSpec, array|string|bool|int $value): array|bool|int|string|object {
- $oneOf = $this->getParamTypeOneOf($paramSpec);
- if (isset($oneOf)) {
- $types = [];
- foreach ($oneOf as $type) {
- if ($type['type'] === 'array' && str_contains($value, ',')) {
- return $this->castParamToArray($type, $value);
+
+ private function getParamType(array $paramSpec): ?string
+ {
+ // @todo File a CXAPI ticket regarding the inconsistent nesting of the 'type' property.
+ if (array_key_exists('type', $paramSpec)) {
+ return $paramSpec['type'];
}
- $types[] = $type['type'];
- }
- if (in_array('integer', $types, TRUE) && ctype_digit($value)) {
- return $this->doCastParamType('integer', $value);
- }
- }
- elseif ($paramSpec['type'] === 'array') {
- if (is_array($value) && count($value) === 1) {
- return $this->castParamToArray($paramSpec, $value[0]);
- }
- return $this->castParamToArray($paramSpec, $value);
+ if (array_key_exists('schema', $paramSpec) && array_key_exists('type', $paramSpec['schema'])) {
+ return $paramSpec['schema']['type'];
+ }
+ return null;
}
- $type = $this->getParamType($paramSpec);
- if (!$type) {
- return $value;
+ private function createCallableValidator(InputArgument $argument, array $params): ?callable
+ {
+ $validator = null;
+ if (array_key_exists($argument->getName(), $params)) {
+ $paramSpec = $params[$argument->getName()];
+ $constraints = [
+ new NotBlank(),
+ ];
+ if ($type = $this->getParamType($paramSpec)) {
+ if (in_array($type, ['int', 'integer'])) {
+ // Need to evaluate whether a string contains only digits.
+ $constraints[] = new Type('digit');
+ } elseif ($type === 'array') {
+ $constraints[] = new Type('string');
+ } else {
+ $constraints[] = new Type($type);
+ }
+ }
+ if (array_key_exists('schema', $paramSpec)) {
+ $schema = $paramSpec['schema'];
+ $constraints = $this->createLengthConstraint($schema, $constraints);
+ $constraints = $this->createRegexConstraint($schema, $constraints);
+ }
+ $validator = $this->createValidatorFromConstraints($constraints);
+ }
+ return $validator;
}
- return $this->doCastParamType($type, $value);
- }
-
- private function doCastParamType(string $type, mixed $value): array|bool|int|string|object {
- return match ($type) {
- 'integer' => (int) $value,
- 'boolean' => $this->castBool($value),
- 'array' => is_string($value) ? explode(',', $value) : (array) $value,
- 'string' => (string) $value,
- 'object' => json_decode($value, FALSE, 512, JSON_THROW_ON_ERROR),
- };
- }
-
- public function castBool(mixed $val): bool {
- return (bool) (is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : $val);
- }
-
- private function getParamType(array $paramSpec): ?string {
- // @todo File a CXAPI ticket regarding the inconsistent nesting of the 'type' property.
- if (array_key_exists('type', $paramSpec)) {
- return $paramSpec['type'];
+ /**
+ * @return array
+ */
+ private function createLengthConstraint(array $schema, array $constraints): array
+ {
+ if (array_key_exists('minLength', $schema) || array_key_exists('maxLength', $schema)) {
+ $lengthOptions = [];
+ if (array_key_exists('minLength', $schema)) {
+ $lengthOptions['min'] = $schema['minLength'];
+ }
+ if (array_key_exists('maxLength', $schema)) {
+ $lengthOptions['max'] = $schema['maxLength'];
+ }
+ $constraints[] = new Length($lengthOptions);
+ }
+ return $constraints;
}
- if (array_key_exists('schema', $paramSpec) && array_key_exists('type', $paramSpec['schema'])) {
- return $paramSpec['schema']['type'];
- }
- return NULL;
- }
-
- private function createCallableValidator(InputArgument $argument, array $params): ?callable {
- $validator = NULL;
- if (array_key_exists($argument->getName(), $params)) {
- $paramSpec = $params[$argument->getName()];
- $constraints = [
- new NotBlank(),
- ];
- if ($type = $this->getParamType($paramSpec)) {
- if (in_array($type, ['int', 'integer'])) {
- // Need to evaluate whether a string contains only digits.
- $constraints[] = new Type('digit');
+ /**
+ * @return array
+ */
+ protected function createRegexConstraint(array $schema, array $constraints): array
+ {
+ if (array_key_exists('format', $schema)) {
+ if ($schema['format'] === 'uuid') {
+ $constraints[] = CommandBase::getUuidRegexConstraint();
+ }
+ } elseif (array_key_exists('pattern', $schema)) {
+ $constraints[] = new Regex([
+ 'message' => 'It must match the pattern ' . $schema['pattern'],
+ 'pattern' => '/' . $schema['pattern'] . '/',
+ ]);
}
- elseif ($type === 'array') {
- $constraints[] = new Type('string');
- }
- else {
- $constraints[] = new Type($type);
- }
- }
- if (array_key_exists('schema', $paramSpec)) {
- $schema = $paramSpec['schema'];
- $constraints = $this->createLengthConstraint($schema, $constraints);
- $constraints = $this->createRegexConstraint($schema, $constraints);
- }
- $validator = $this->createValidatorFromConstraints($constraints);
+ return $constraints;
}
- return $validator;
- }
-
- /**
- * @return array
- */
- private function createLengthConstraint(array $schema, array $constraints): array {
- if (array_key_exists('minLength', $schema) || array_key_exists('maxLength', $schema)) {
- $lengthOptions = [];
- if (array_key_exists('minLength', $schema)) {
- $lengthOptions['min'] = $schema['minLength'];
- }
- if (array_key_exists('maxLength', $schema)) {
- $lengthOptions['max'] = $schema['maxLength'];
- }
- $constraints[] = new Length($lengthOptions);
+
+ private function createValidatorFromConstraints(array $constraints): Closure
+ {
+ return static function (mixed $value) use ($constraints) {
+ $violations = Validation::createValidator()
+ ->validate($value, $constraints);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ return $value;
+ };
}
- return $constraints;
- }
-
- /**
- * @return array
- */
- protected function createRegexConstraint(array $schema, array $constraints): array {
- if (array_key_exists('format', $schema)) {
- if ($schema['format'] === 'uuid') {
- $constraints[] = CommandBase::getUuidRegexConstraint();
- }
+
+ protected function addQueryParamsToClient(InputInterface $input, Client $acquiaCloudClient): void
+ {
+ if ($this->queryParams) {
+ foreach ($this->queryParams as $key => $paramSpec) {
+ // We may have a queryParam that is used in the path rather than the query string.
+ if ($input->hasOption($key) && $input->getOption($key) !== null) {
+ $acquiaCloudClient->addQuery($key, $input->getOption($key));
+ } elseif ($input->hasArgument($key) && $input->getArgument($key) !== null) {
+ $acquiaCloudClient->addQuery($key, $input->getArgument($key));
+ }
+ }
+ }
}
- elseif (array_key_exists('pattern', $schema)) {
- $constraints[] = new Regex([
- 'message' => 'It must match the pattern ' . $schema['pattern'],
- 'pattern' => '/' . $schema['pattern'] . '/',
- ]);
+
+ private function addPostParamsToClient(InputInterface $input, Client $acquiaCloudClient): void
+ {
+ if ($this->postParams) {
+ foreach ($this->postParams as $paramName => $paramSpec) {
+ $paramValue = $this->getParamFromInput($input, $paramName);
+ if (!is_null($paramValue)) {
+ $this->addPostParamToClient($paramName, $paramSpec, $paramValue, $acquiaCloudClient);
+ }
+ }
+ }
}
- return $constraints;
- }
-
- private function createValidatorFromConstraints(array $constraints): Closure {
- return static function (mixed $value) use ($constraints) {
- $violations = Validation::createValidator()
- ->validate($value, $constraints);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
- }
- return $value;
- };
- }
-
- protected function addQueryParamsToClient(InputInterface $input, Client $acquiaCloudClient): void {
- if ($this->queryParams) {
- foreach ($this->queryParams as $key => $paramSpec) {
- // We may have a queryParam that is used in the path rather than the query string.
- if ($input->hasOption($key) && $input->getOption($key) !== NULL) {
- $acquiaCloudClient->addQuery($key, $input->getOption($key));
+
+ /**
+ * @param array|null $paramSpec
+ */
+ private function addPostParamToClient(string $paramName, ?array $paramSpec, mixed $paramValue, Client $acquiaCloudClient): void
+ {
+ $paramName = ApiCommandHelper::restoreRenamedParameter($paramName);
+ if ($paramSpec) {
+ $paramValue = $this->castParamType($paramSpec, $paramValue);
}
- elseif ($input->hasArgument($key) && $input->getArgument($key) !== NULL) {
- $acquiaCloudClient->addQuery($key, $input->getArgument($key));
+ if ($paramSpec && array_key_exists('format', $paramSpec) && $paramSpec["format"] === 'binary') {
+ $acquiaCloudClient->addOption('multipart', [
+ [
+ 'contents' => Utils::tryFopen($paramValue, 'r'),
+ 'name' => $paramName,
+ ],
+ ]);
+ } else {
+ $acquiaCloudClient->addOption('json', [$paramName => $paramValue]);
}
- }
}
- }
-
- private function addPostParamsToClient(InputInterface $input, Client $acquiaCloudClient): void {
- if ($this->postParams) {
- foreach ($this->postParams as $paramName => $paramSpec) {
- $paramValue = $this->getParamFromInput($input, $paramName);
- if (!is_null($paramValue)) {
- $this->addPostParamToClient($paramName, $paramSpec, $paramValue, $acquiaCloudClient);
+
+ private function askFreeFormQuestion(InputArgument $argument, array $params): mixed
+ {
+ // Default value may be an empty array, which causes Question to choke.
+ $default = $argument->getDefault() ?: null;
+ $question = new Question("Enter a value for {$argument->getName()}", $default);
+ switch ($argument->getName()) {
+ case 'applicationUuid':
+ // @todo Provide a list of application UUIDs.
+ $question->setValidator(function (mixed $value) {
+ return $this->validateApplicationUuid($value);
+ });
+ break;
+ case 'environmentId':
+ // @todo Provide a list of environment IDs.
+ case 'source':
+ $question->setValidator(function (mixed $value) use ($argument): string {
+ return $this->validateEnvironmentUuid($value, $argument->getName());
+ });
+ break;
+
+ default:
+ $validator = $this->createCallableValidator($argument, $params);
+ $question->setValidator($validator);
+ break;
}
- }
- }
- }
-
- /**
- * @param array|null $paramSpec
- */
- private function addPostParamToClient(string $paramName, ?array $paramSpec, mixed $paramValue, Client $acquiaCloudClient): void {
- $paramName = ApiCommandHelper::restoreRenamedParameter($paramName);
- if ($paramSpec) {
- $paramValue = $this->castParamType($paramSpec, $paramValue);
- }
- if ($paramSpec && array_key_exists('format', $paramSpec) && $paramSpec["format"] === 'binary') {
- $acquiaCloudClient->addOption('multipart', [
- [
- 'contents' => Utils::tryFopen($paramValue, 'r'),
- 'name' => $paramName,
- ],
- ]);
- }
- else {
- $acquiaCloudClient->addOption('json', [$paramName => $paramValue]);
- }
- }
-
- private function askFreeFormQuestion(InputArgument $argument, array $params): mixed {
- // Default value may be an empty array, which causes Question to choke.
- $default = $argument->getDefault() ?: NULL;
- $question = new Question("Enter a value for {$argument->getName()}", $default);
- switch ($argument->getName()) {
- case 'applicationUuid':
- // @todo Provide a list of application UUIDs.
- $question->setValidator(function (mixed $value) {
- return $this->validateApplicationUuid($value);
- });
- break;
- case 'environmentId':
- // @todo Provide a list of environment IDs.
- case 'source':
- $question->setValidator(function (mixed $value) use ($argument): string {
- return $this->validateEnvironmentUuid($value, $argument->getName());
- });
- break;
-
- default:
- $validator = $this->createCallableValidator($argument, $params);
- $question->setValidator($validator);
- break;
- }
- // Allow unlimited attempts.
- $question->setMaxAttempts(NULL);
- return $this->io->askQuestion($question);
- }
-
- /**
- * @return null|array
- */
- private function getParamTypeOneOf(array $paramSpec): ?array {
- $oneOf = $paramSpec['oneOf'] ?? NULL;
- if (array_key_exists('schema', $paramSpec) && array_key_exists('oneOf', $paramSpec['schema'])) {
- $oneOf = $paramSpec['schema']['oneOf'];
+ // Allow unlimited attempts.
+ $question->setMaxAttempts(null);
+ return $this->io->askQuestion($question);
}
- return $oneOf;
- }
-
- private function castParamToArray(array $paramSpec, array|string $originalValue): string|array|bool|int {
- if (array_key_exists('items', $paramSpec) && array_key_exists('type', $paramSpec['items'])) {
- if (!is_array($originalValue)) {
- $originalValue = $this->doCastParamType('array', $originalValue);
- }
- $itemType = $paramSpec['items']['type'];
- $array = [];
- foreach ($originalValue as $key => $v) {
- $array[$key] = $this->doCastParamType($itemType, $v);
- }
- return $array;
+
+ /**
+ * @return null|array
+ */
+ private function getParamTypeOneOf(array $paramSpec): ?array
+ {
+ $oneOf = $paramSpec['oneOf'] ?? null;
+ if (array_key_exists('schema', $paramSpec) && array_key_exists('oneOf', $paramSpec['schema'])) {
+ $oneOf = $paramSpec['schema']['oneOf'];
+ }
+ return $oneOf;
}
- return $this->doCastParamType('array', $originalValue);
- }
+ private function castParamToArray(array $paramSpec, array|string $originalValue): string|array|bool|int
+ {
+ if (array_key_exists('items', $paramSpec) && array_key_exists('type', $paramSpec['items'])) {
+ if (!is_array($originalValue)) {
+ $originalValue = $this->doCastParamType('array', $originalValue);
+ }
+ $itemType = $paramSpec['items']['type'];
+ $array = [];
+ foreach ($originalValue as $key => $v) {
+ $array[$key] = $this->doCastParamType($itemType, $v);
+ }
+ return $array;
+ }
+ return $this->doCastParamType('array', $originalValue);
+ }
}
diff --git a/src/Command/Api/ApiCommandFactory.php b/src/Command/Api/ApiCommandFactory.php
index b9821c55a..ed226121f 100644
--- a/src/Command/Api/ApiCommandFactory.php
+++ b/src/Command/Api/ApiCommandFactory.php
@@ -1,6 +1,6 @@
localMachineHelper,
- $this->datastoreCloud,
- $this->datastoreAcli,
- $this->cloudCredentials,
- $this->telemetryHelper,
- $this->projectDir,
- $this->cloudApiClientService,
- $this->sshHelper,
- $this->sshDir,
- $this->logger,
- );
- }
-
- public function createListCommand(): ApiListCommand {
- return new ApiListCommand(
- $this->localMachineHelper,
- $this->datastoreCloud,
- $this->datastoreAcli,
- $this->cloudCredentials,
- $this->telemetryHelper,
- $this->projectDir,
- $this->cloudApiClientService,
- $this->sshHelper,
- $this->sshDir,
- $this->logger,
- );
- }
+ public function createCommand(): ApiBaseCommand
+ {
+ return new ApiBaseCommand(
+ $this->localMachineHelper,
+ $this->datastoreCloud,
+ $this->datastoreAcli,
+ $this->cloudCredentials,
+ $this->telemetryHelper,
+ $this->projectDir,
+ $this->cloudApiClientService,
+ $this->sshHelper,
+ $this->sshDir,
+ $this->logger,
+ );
+ }
+ public function createListCommand(): ApiListCommand
+ {
+ return new ApiListCommand(
+ $this->localMachineHelper,
+ $this->datastoreCloud,
+ $this->datastoreAcli,
+ $this->cloudCredentials,
+ $this->telemetryHelper,
+ $this->projectDir,
+ $this->cloudApiClientService,
+ $this->sshHelper,
+ $this->sshDir,
+ $this->logger,
+ );
+ }
}
diff --git a/src/Command/Api/ApiCommandHelper.php b/src/Command/Api/ApiCommandHelper.php
index bb807ddbe..867a3d84a 100644
--- a/src/Command/Api/ApiCommandHelper.php
+++ b/src/Command/Api/ApiCommandHelper.php
@@ -1,6 +1,6 @@
- */
- public function getApiCommands(string $acquiaCloudSpecFilePath, string $commandPrefix, CommandFactoryInterface $commandFactory): array {
- $acquiaCloudSpec = $this->getCloudApiSpec($acquiaCloudSpecFilePath);
- $apiCommands = $this->generateApiCommandsFromSpec($acquiaCloudSpec, $commandPrefix, $commandFactory);
- $apiListCommands = $this->generateApiListCommands($apiCommands, $commandPrefix, $commandFactory);
- return array_merge($apiCommands, $apiListCommands);
- }
-
- private function useCloudApiSpecCache(): bool {
- return !(getenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE') === '0');
- }
-
- protected function addArgumentExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): mixed {
- if (array_key_exists('example', $paramDefinition)) {
- if (is_array($paramDefinition['example'])) {
- $usage = reset($paramDefinition['example']);
- }
- elseif (is_string($paramDefinition['example']) && str_contains($paramDefinition['example'], ' ')) {
- $usage .= '"' . $paramDefinition['example'] . '" ';
- }
- else {
- $usage .= $paramDefinition['example'] . ' ';
- }
+class ApiCommandHelper
+{
+ public function __construct(
+ private ConsoleLogger $logger
+ ) {
}
- return $usage;
- }
+ /**
+ * @return array
+ */
+ public function getApiCommands(string $acquiaCloudSpecFilePath, string $commandPrefix, CommandFactoryInterface $commandFactory): array
+ {
+ $acquiaCloudSpec = $this->getCloudApiSpec($acquiaCloudSpecFilePath);
+ $apiCommands = $this->generateApiCommandsFromSpec($acquiaCloudSpec, $commandPrefix, $commandFactory);
+ $apiListCommands = $this->generateApiListCommands($apiCommands, $commandPrefix, $commandFactory);
+ return array_merge($apiCommands, $apiListCommands);
+ }
- private function addOptionExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): string {
- if (array_key_exists('example', $paramDefinition)) {
- $usage .= '--' . $paramDefinition['name'] . '="' . $paramDefinition['example'] . '" ';
+ private function useCloudApiSpecCache(): bool
+ {
+ return !(getenv('ACQUIA_CLI_USE_CLOUD_API_SPEC_CACHE') === '0');
}
- return $usage;
- }
-
- private function addApiCommandParameters(array $schema, array $acquiaCloudSpec, ApiBaseCommand $command): void {
- $inputDefinition = [];
- $usage = '';
-
- // Parameters to be used in the request query and path.
- if (array_key_exists('parameters', $schema)) {
- [$queryInputDefinition, $queryParamUsageSuffix] = $this->addApiCommandParametersForPathAndQuery($schema, $acquiaCloudSpec);
- /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */
- foreach ($queryInputDefinition as $parameterDefinition) {
- $parameterSpecification = $this->getParameterDefinitionFromSpec($parameterDefinition->getName(), $acquiaCloudSpec, $schema);
- if ($parameterSpecification['in'] === 'query') {
- $command->addQueryParameter($parameterDefinition->getName(), $parameterSpecification);
- }
- elseif ($parameterSpecification['in'] === 'path') {
- $command->addPathParameter($parameterDefinition->getName(), $parameterSpecification);
+ protected function addArgumentExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): mixed
+ {
+ if (array_key_exists('example', $paramDefinition)) {
+ if (is_array($paramDefinition['example'])) {
+ $usage = reset($paramDefinition['example']);
+ } elseif (is_string($paramDefinition['example']) && str_contains($paramDefinition['example'], ' ')) {
+ $usage .= '"' . $paramDefinition['example'] . '" ';
+ } else {
+ $usage .= $paramDefinition['example'] . ' ';
+ }
}
- }
- $usage .= $queryParamUsageSuffix;
- $inputDefinition = array_merge($inputDefinition, $queryInputDefinition);
- }
- // Parameters to be used in the request body.
- if (array_key_exists('requestBody', $schema)) {
- [
- $bodyInputDefinition,
- $requestBodyParamUsageSuffix,
- ] = $this->addApiCommandParametersForRequestBody($schema, $acquiaCloudSpec);
- $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec);
- /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */
- foreach ($bodyInputDefinition as $parameterDefinition) {
- $parameterSpecification = $this->getPropertySpecFromRequestBodyParam($requestBodySchema, $parameterDefinition);
- $command->addPostParameter($parameterDefinition->getName(), $parameterSpecification);
- }
- $usage .= $requestBodyParamUsageSuffix;
- $inputDefinition = array_merge($inputDefinition, $bodyInputDefinition);
+ return $usage;
}
- $command->setDefinition(new InputDefinition($inputDefinition));
- if ($usage) {
- $command->addUsage(rtrim($usage));
- }
- $this->addAliasUsageExamples($command, $inputDefinition, rtrim($usage));
- }
-
- /**
- * @return array
- */
- private function addApiCommandParametersForRequestBody(array $schema, array $acquiaCloudSpec): array {
- $usage = '';
- $inputDefinition = [];
- $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec);
-
- if (!array_key_exists('properties', $requestBodySchema)) {
- return [];
+ private function addOptionExampleToUsageForGetEndpoint(array $paramDefinition, string $usage): string
+ {
+ if (array_key_exists('example', $paramDefinition)) {
+ $usage .= '--' . $paramDefinition['name'] . '="' . $paramDefinition['example'] . '" ';
+ }
+
+ return $usage;
}
- foreach ($requestBodySchema['properties'] as $propKey => $paramDefinition) {
- $isRequired = array_key_exists('required', $requestBodySchema) && in_array($propKey, $requestBodySchema['required'], TRUE);
- $propKey = self::renameParameter($propKey);
- if ($isRequired) {
- if (!array_key_exists('description', $paramDefinition)) {
- $description = $paramDefinition["additionalProperties"]["description"];
+ private function addApiCommandParameters(array $schema, array $acquiaCloudSpec, ApiBaseCommand $command): void
+ {
+ $inputDefinition = [];
+ $usage = '';
+
+ // Parameters to be used in the request query and path.
+ if (array_key_exists('parameters', $schema)) {
+ [$queryInputDefinition, $queryParamUsageSuffix] = $this->addApiCommandParametersForPathAndQuery($schema, $acquiaCloudSpec);
+ /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */
+ foreach ($queryInputDefinition as $parameterDefinition) {
+ $parameterSpecification = $this->getParameterDefinitionFromSpec($parameterDefinition->getName(), $acquiaCloudSpec, $schema);
+ if ($parameterSpecification['in'] === 'query') {
+ $command->addQueryParameter($parameterDefinition->getName(), $parameterSpecification);
+ } elseif ($parameterSpecification['in'] === 'path') {
+ $command->addPathParameter($parameterDefinition->getName(), $parameterSpecification);
+ }
+ }
+ $usage .= $queryParamUsageSuffix;
+ $inputDefinition = array_merge($inputDefinition, $queryInputDefinition);
+ }
+
+ // Parameters to be used in the request body.
+ if (array_key_exists('requestBody', $schema)) {
+ [
+ $bodyInputDefinition,
+ $requestBodyParamUsageSuffix,
+ ] = $this->addApiCommandParametersForRequestBody($schema, $acquiaCloudSpec);
+ $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec);
+ /** @var \Symfony\Component\Console\Input\InputOption|InputArgument $parameterDefinition */
+ foreach ($bodyInputDefinition as $parameterDefinition) {
+ $parameterSpecification = $this->getPropertySpecFromRequestBodyParam($requestBodySchema, $parameterDefinition);
+ $command->addPostParameter($parameterDefinition->getName(), $parameterSpecification);
+ }
+ $usage .= $requestBodyParamUsageSuffix;
+ $inputDefinition = array_merge($inputDefinition, $bodyInputDefinition);
}
- else {
- $description = $paramDefinition['description'];
+
+ $command->setDefinition(new InputDefinition($inputDefinition));
+ if ($usage) {
+ $command->addUsage(rtrim($usage));
}
- $inputDefinition[] = new InputArgument(
- $propKey,
- array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputArgument::IS_ARRAY | InputArgument::REQUIRED : InputArgument::REQUIRED,
- $description
- );
- $usage = $this->addPostArgumentUsageToExample($schema['requestBody'], $propKey, $paramDefinition, 'argument', $usage);
- }
- else {
- $inputDefinition[] = new InputOption(
- $propKey,
- NULL,
- array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED : InputOption::VALUE_REQUIRED,
- array_key_exists('description', $paramDefinition) ? $paramDefinition['description'] : $propKey
- );
- $usage = $this->addPostArgumentUsageToExample($schema["requestBody"], $propKey, $paramDefinition, 'option', $usage);
- // @todo Add validator for $param['enum'] values?
- }
- }
- /** @var \Symfony\Component\Console\Input\InputArgument|InputOption $parameterDefinition */
- foreach ($inputDefinition as $index => $parameterDefinition) {
- if ($parameterDefinition->isArray()) {
- // Move to the end of the array.
- unset($inputDefinition[$index]);
- $inputDefinition[] = $parameterDefinition;
- }
+ $this->addAliasUsageExamples($command, $inputDefinition, rtrim($usage));
}
- return [$inputDefinition, $usage];
- }
-
- private function addPostArgumentUsageToExample(mixed $requestBody, mixed $propKey, mixed $paramDefinition, string $type, string $usage): string {
- $requestBodyContent = $this->getRequestBodyContent($requestBody);
-
- if (array_key_exists('example', $requestBodyContent)) {
- $example = $requestBodyContent['example'];
- $prefix = $type === 'argument' ? '' : "--{$propKey}=";
- if (array_key_exists($propKey, $example)) {
- switch ($paramDefinition['type']) {
- case 'object':
- $usage .= $prefix . '"' . json_encode($example[$propKey], JSON_THROW_ON_ERROR) . '"" ';
- break;
-
- case 'array':
- $isMultidimensional = count($example[$propKey]) !== count($example[$propKey], COUNT_RECURSIVE);
- if (!$isMultidimensional) {
- foreach ($example[$propKey] as $value) {
- $usage .= $prefix . "\"$value\" ";
- }
+ /**
+ * @return array
+ */
+ private function addApiCommandParametersForRequestBody(array $schema, array $acquiaCloudSpec): array
+ {
+ $usage = '';
+ $inputDefinition = [];
+ $requestBodySchema = $this->getRequestBodyFromParameterSchema($schema, $acquiaCloudSpec);
+
+ if (!array_key_exists('properties', $requestBodySchema)) {
+ return [];
+ }
+ foreach ($requestBodySchema['properties'] as $propKey => $paramDefinition) {
+ $isRequired = array_key_exists('required', $requestBodySchema) && in_array($propKey, $requestBodySchema['required'], true);
+ $propKey = self::renameParameter($propKey);
+
+ if ($isRequired) {
+ if (!array_key_exists('description', $paramDefinition)) {
+ $description = $paramDefinition["additionalProperties"]["description"];
+ } else {
+ $description = $paramDefinition['description'];
+ }
+ $inputDefinition[] = new InputArgument(
+ $propKey,
+ array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputArgument::IS_ARRAY | InputArgument::REQUIRED : InputArgument::REQUIRED,
+ $description
+ );
+ $usage = $this->addPostArgumentUsageToExample($schema['requestBody'], $propKey, $paramDefinition, 'argument', $usage);
+ } else {
+ $inputDefinition[] = new InputOption(
+ $propKey,
+ null,
+ array_key_exists('type', $paramDefinition) && $paramDefinition['type'] === 'array' ? InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED : InputOption::VALUE_REQUIRED,
+ array_key_exists('description', $paramDefinition) ? $paramDefinition['description'] : $propKey
+ );
+ $usage = $this->addPostArgumentUsageToExample($schema["requestBody"], $propKey, $paramDefinition, 'option', $usage);
+ // @todo Add validator for $param['enum'] values?
}
- else {
- // @todo Pretty sure prevents the user from using the arguments.
- // Probably a bug. How can we allow users to specify a multidimensional array as an
- // argument?
- $value = json_encode($example[$propKey], JSON_THROW_ON_ERROR);
- $usage .= $prefix . "\"$value\" ";
+ }
+ /** @var \Symfony\Component\Console\Input\InputArgument|InputOption $parameterDefinition */
+ foreach ($inputDefinition as $index => $parameterDefinition) {
+ if ($parameterDefinition->isArray()) {
+ // Move to the end of the array.
+ unset($inputDefinition[$index]);
+ $inputDefinition[] = $parameterDefinition;
}
- break;
+ }
- case 'string':
- case 'boolean':
- case 'integer':
- if (is_array($example[$propKey])) {
- $value = reset($example[$propKey]);
- }
- else {
- $value = $example[$propKey];
+ return [$inputDefinition, $usage];
+ }
+
+ private function addPostArgumentUsageToExample(mixed $requestBody, mixed $propKey, mixed $paramDefinition, string $type, string $usage): string
+ {
+ $requestBodyContent = $this->getRequestBodyContent($requestBody);
+
+ if (array_key_exists('example', $requestBodyContent)) {
+ $example = $requestBodyContent['example'];
+ $prefix = $type === 'argument' ? '' : "--{$propKey}=";
+ if (array_key_exists($propKey, $example)) {
+ switch ($paramDefinition['type']) {
+ case 'object':
+ $usage .= $prefix . '"' . json_encode($example[$propKey], JSON_THROW_ON_ERROR) . '"" ';
+ break;
+
+ case 'array':
+ $isMultidimensional = count($example[$propKey]) !== count($example[$propKey], COUNT_RECURSIVE);
+ if (!$isMultidimensional) {
+ foreach ($example[$propKey] as $value) {
+ $usage .= $prefix . "\"$value\" ";
+ }
+ } else {
+ // @todo Pretty sure prevents the user from using the arguments.
+ // Probably a bug. How can we allow users to specify a multidimensional array as an
+ // argument?
+ $value = json_encode($example[$propKey], JSON_THROW_ON_ERROR);
+ $usage .= $prefix . "\"$value\" ";
+ }
+ break;
+
+ case 'string':
+ case 'boolean':
+ case 'integer':
+ if (is_array($example[$propKey])) {
+ $value = reset($example[$propKey]);
+ } else {
+ $value = $example[$propKey];
+ }
+ $usage .= $prefix . "\"{$value}\" ";
+ break;
+ }
}
- $usage .= $prefix . "\"{$value}\" ";
- break;
}
- }
- }
- return $usage;
- }
-
- /**
- * @return array
- */
- private function addApiCommandParametersForPathAndQuery(array $schema, array $acquiaCloudSpec): array {
- $usage = '';
- $inputDefinition = [];
- if (!array_key_exists('parameters', $schema)) {
- return [];
- }
- foreach ($schema['parameters'] as $parameter) {
- if (array_key_exists('$ref', $parameter)) {
- $parts = explode('/', $parameter['$ref']);
- $paramKey = end($parts);
- $paramDefinition = $this->getParameterDefinitionFromSpec($paramKey, $acquiaCloudSpec, $schema);
- }
- else {
- $paramDefinition = $parameter;
- }
-
- $required = array_key_exists('required', $paramDefinition) && $paramDefinition['required'];
- $this->addAliasParameterDescriptions($paramDefinition);
- if ($required) {
- $inputDefinition[] = new InputArgument(
- $paramDefinition['name'],
- InputArgument::REQUIRED,
- $paramDefinition['description']
- );
- $usage = $this->addArgumentExampleToUsageForGetEndpoint($paramDefinition, $usage);
- }
- else {
- $inputDefinition[] = new InputOption(
- $paramDefinition['name'],
- NULL,
- InputOption::VALUE_REQUIRED,
- $paramDefinition['description']
- );
- $usage = $this->addOptionExampleToUsageForGetEndpoint($paramDefinition, $usage);
- }
+ return $usage;
}
- return [$inputDefinition, $usage];
- }
+ /**
+ * @return array
+ */
+ private function addApiCommandParametersForPathAndQuery(array $schema, array $acquiaCloudSpec): array
+ {
+ $usage = '';
+ $inputDefinition = [];
+ if (!array_key_exists('parameters', $schema)) {
+ return [];
+ }
+ foreach ($schema['parameters'] as $parameter) {
+ if (array_key_exists('$ref', $parameter)) {
+ $parts = explode('/', $parameter['$ref']);
+ $paramKey = end($parts);
+ $paramDefinition = $this->getParameterDefinitionFromSpec($paramKey, $acquiaCloudSpec, $schema);
+ } else {
+ $paramDefinition = $parameter;
+ }
+
+ $required = array_key_exists('required', $paramDefinition) && $paramDefinition['required'];
+ $this->addAliasParameterDescriptions($paramDefinition);
+ if ($required) {
+ $inputDefinition[] = new InputArgument(
+ $paramDefinition['name'],
+ InputArgument::REQUIRED,
+ $paramDefinition['description']
+ );
+ $usage = $this->addArgumentExampleToUsageForGetEndpoint($paramDefinition, $usage);
+ } else {
+ $inputDefinition[] = new InputOption(
+ $paramDefinition['name'],
+ null,
+ InputOption::VALUE_REQUIRED,
+ $paramDefinition['description']
+ );
+ $usage = $this->addOptionExampleToUsageForGetEndpoint($paramDefinition, $usage);
+ }
+ }
- private function getParameterDefinitionFromSpec(string $paramKey, array $acquiaCloudSpec, mixed $schema): mixed {
- $uppercaseKey = ucfirst($paramKey);
- if (array_key_exists('parameters', $acquiaCloudSpec['components'])
- && array_key_exists($uppercaseKey, $acquiaCloudSpec['components']['parameters'])) {
- return $acquiaCloudSpec['components']['parameters'][$uppercaseKey];
- }
- foreach ($schema['parameters'] as $parameter) {
- if ($parameter['name'] === $paramKey) {
- return $parameter;
- }
- }
- return NULL;
- }
-
- private function getParameterSchemaFromSpec(string $paramKey, array $acquiaCloudSpec): mixed {
- return $acquiaCloudSpec['components']['schemas'][$paramKey];
- }
-
- // @infection-ignore-all
- private function isApiSpecChecksumCacheValid(\Symfony\Component\Cache\CacheItem $cacheItem, string $acquiaCloudSpecFileChecksum): bool {
- // If the spec file doesn't exist, assume cache is valid.
- if (!$acquiaCloudSpecFileChecksum && $cacheItem->isHit()) {
- return TRUE;
- }
- // If there's an invalid entry OR there's no entry, return false.
- if (!$cacheItem->isHit() || $cacheItem->get() !== $acquiaCloudSpecFileChecksum) {
- return FALSE;
+ return [$inputDefinition, $usage];
}
- return TRUE;
- }
-
- /**
- * Breaking caching during tests will cause performance issues and timeouts.
- *
- * @return array
- * @infection-ignore-all
- */
- private function getCloudApiSpec(string $specFilePath): array {
- $cacheKey = basename($specFilePath);
- $cache = new PhpArrayAdapter(__DIR__ . '/../../../var/cache/' . $cacheKey . '.cache', new NullAdapter());
- $cacheItemChecksum = $cache->getItem($cacheKey . '.checksum');
- $cacheItemSpec = $cache->getItem($cacheKey);
-
- // When running the phar, the original file may not exist. In that case, always use the cache.
- if (!file_exists($specFilePath) && $cacheItemSpec->isHit()) {
- return $cacheItemSpec->get();
+
+ private function getParameterDefinitionFromSpec(string $paramKey, array $acquiaCloudSpec, mixed $schema): mixed
+ {
+ $uppercaseKey = ucfirst($paramKey);
+ if (
+ array_key_exists('parameters', $acquiaCloudSpec['components'])
+ && array_key_exists($uppercaseKey, $acquiaCloudSpec['components']['parameters'])
+ ) {
+ return $acquiaCloudSpec['components']['parameters'][$uppercaseKey];
+ }
+ foreach ($schema['parameters'] as $parameter) {
+ if ($parameter['name'] === $paramKey) {
+ return $parameter;
+ }
+ }
+ return null;
}
- // Otherwise, only use cache when it is valid.
- $checksum = md5_file($specFilePath);
- // @infection-ignore-all
- if ($this->useCloudApiSpecCache()
- && $this->isApiSpecChecksumCacheValid($cacheItemChecksum, $checksum) && $cacheItemSpec->isHit()
- ) {
- return $cacheItemSpec->get();
+ private function getParameterSchemaFromSpec(string $paramKey, array $acquiaCloudSpec): mixed
+ {
+ return $acquiaCloudSpec['components']['schemas'][$paramKey];
}
- // Parse file. This can take a long while!
- $this->logger->debug("Rebuilding caches...");
- $spec = json_decode(file_get_contents($specFilePath), TRUE);
-
- $cache->warmUp([
- $cacheKey => $spec,
- $cacheKey . '.checksum' => $checksum,
- ]);
-
- return $spec;
- }
-
- /**
- * @return ApiBaseCommand[]
- */
- private function generateApiCommandsFromSpec(array $acquiaCloudSpec, string $commandPrefix, CommandFactoryInterface $commandFactory): array {
- $apiCommands = [];
- foreach ($acquiaCloudSpec['paths'] as $path => $endpoint) {
- foreach ($endpoint as $method => $schema) {
- if (!array_key_exists('x-cli-name', $schema)) {
- continue;
+ // @infection-ignore-all
+ private function isApiSpecChecksumCacheValid(\Symfony\Component\Cache\CacheItem $cacheItem, string $acquiaCloudSpecFileChecksum): bool
+ {
+ // If the spec file doesn't exist, assume cache is valid.
+ if (!$acquiaCloudSpecFileChecksum && $cacheItem->isHit()) {
+ return true;
}
+ // If there's an invalid entry OR there's no entry, return false.
+ if (!$cacheItem->isHit() || $cacheItem->get() !== $acquiaCloudSpecFileChecksum) {
+ return false;
+ }
+ return true;
+ }
- if (in_array($schema['x-cli-name'], $this->getSkippedApiCommands(), TRUE)) {
- continue;
+ /**
+ * Breaking caching during tests will cause performance issues and timeouts.
+ *
+ * @return array
+ * @infection-ignore-all
+ */
+ private function getCloudApiSpec(string $specFilePath): array
+ {
+ $cacheKey = basename($specFilePath);
+ $cache = new PhpArrayAdapter(__DIR__ . '/../../../var/cache/' . $cacheKey . '.cache', new NullAdapter());
+ $cacheItemChecksum = $cache->getItem($cacheKey . '.checksum');
+ $cacheItemSpec = $cache->getItem($cacheKey);
+
+ // When running the phar, the original file may not exist. In that case, always use the cache.
+ if (!file_exists($specFilePath) && $cacheItemSpec->isHit()) {
+ return $cacheItemSpec->get();
}
- // Skip deprecated endpoints.
+ // Otherwise, only use cache when it is valid.
+ $checksum = md5_file($specFilePath);
// @infection-ignore-all
- if (array_key_exists('deprecated', $schema) && $schema['deprecated']) {
- continue;
+ if (
+ $this->useCloudApiSpecCache()
+ && $this->isApiSpecChecksumCacheValid($cacheItemChecksum, $checksum) && $cacheItemSpec->isHit()
+ ) {
+ return $cacheItemSpec->get();
}
- $commandName = $commandPrefix . ':' . $schema['x-cli-name'];
- $command = $commandFactory->createCommand();
- $command->setName($commandName);
- $command->setDescription($schema['summary']);
- $command->setMethod($method);
- $command->setResponses($schema['responses']);
- $command->setHidden(FALSE);
- if (array_key_exists('servers', $acquiaCloudSpec)) {
- $command->setServers($acquiaCloudSpec['servers']);
+ // Parse file. This can take a long while!
+ $this->logger->debug("Rebuilding caches...");
+ $spec = json_decode(file_get_contents($specFilePath), true);
+
+ $cache->warmUp([
+ $cacheKey => $spec,
+ $cacheKey . '.checksum' => $checksum,
+ ]);
+
+ return $spec;
+ }
+
+ /**
+ * @return ApiBaseCommand[]
+ */
+ private function generateApiCommandsFromSpec(array $acquiaCloudSpec, string $commandPrefix, CommandFactoryInterface $commandFactory): array
+ {
+ $apiCommands = [];
+ foreach ($acquiaCloudSpec['paths'] as $path => $endpoint) {
+ foreach ($endpoint as $method => $schema) {
+ if (!array_key_exists('x-cli-name', $schema)) {
+ continue;
+ }
+
+ if (in_array($schema['x-cli-name'], $this->getSkippedApiCommands(), true)) {
+ continue;
+ }
+
+ // Skip deprecated endpoints.
+ // @infection-ignore-all
+ if (array_key_exists('deprecated', $schema) && $schema['deprecated']) {
+ continue;
+ }
+
+ $commandName = $commandPrefix . ':' . $schema['x-cli-name'];
+ $command = $commandFactory->createCommand();
+ $command->setName($commandName);
+ $command->setDescription($schema['summary']);
+ $command->setMethod($method);
+ $command->setResponses($schema['responses']);
+ $command->setHidden(false);
+ if (array_key_exists('servers', $acquiaCloudSpec)) {
+ $command->setServers($acquiaCloudSpec['servers']);
+ }
+ $command->setPath($path);
+ $command->setHelp("For more help, see https://cloudapi-docs.acquia.com/ or https://dev.acquia.com/api-documentation/acquia-cloud-site-factory-api for acsf commands.");
+ $this->addApiCommandParameters($schema, $acquiaCloudSpec, $command);
+ $apiCommands[] = $command;
+ }
}
- $command->setPath($path);
- $command->setHelp("For more help, see https://cloudapi-docs.acquia.com/ or https://dev.acquia.com/api-documentation/acquia-cloud-site-factory-api for acsf commands.");
- $this->addApiCommandParameters($schema, $acquiaCloudSpec, $command);
- $apiCommands[] = $command;
- }
+
+ return $apiCommands;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getSkippedApiCommands(): array
+ {
+ return [
+ // Skip accounts:drush-aliases since we have remote:aliases:download instead and it actually returns
+ // application/gzip content.
+ 'accounts:drush-aliases',
+ // Skip any command that has a duplicative corresponding ACLI command.
+ 'ide:create',
+ 'log:tail',
+ 'ssh-key:create',
+ 'ssh-key:create-upload',
+ 'ssh-key:delete',
+ 'ssh-key:list',
+ 'ssh-key:upload',
+ // Skip buggy or unsupported endpoints.
+ 'environments:stack-metrics-data-metric',
+ ];
}
- return $apiCommands;
- }
-
- /**
- * @return array
- */
- protected function getSkippedApiCommands(): array {
- return [
- // Skip accounts:drush-aliases since we have remote:aliases:download instead and it actually returns
- // application/gzip content.
- 'accounts:drush-aliases',
- // Skip any command that has a duplicative corresponding ACLI command.
- 'ide:create',
- 'log:tail',
- 'ssh-key:create',
- 'ssh-key:create-upload',
- 'ssh-key:delete',
- 'ssh-key:list',
- 'ssh-key:upload',
- // Skip buggy or unsupported endpoints.
- 'environments:stack-metrics-data-metric',
- ];
- }
-
- private function addAliasUsageExamples(ApiBaseCommand $command, array $inputDefinition, string $usage): void {
- foreach ($inputDefinition as $key => $parameter) {
- if ($parameter->getName() === 'applicationUuid') {
- $usageParts = explode(' ', $usage);
- $usageParts[$key] = "myapp";
- $usage = implode(' ', $usageParts);
- $command->addUsage($usage);
- }
- if ($parameter->getName() === 'environmentId') {
- $usageParts = explode(' ', $usage);
- $usageParts[$key] = "myapp.dev";
- $usage = implode(' ', $usageParts);
- $command->addUsage($usage);
- }
+ private function addAliasUsageExamples(ApiBaseCommand $command, array $inputDefinition, string $usage): void
+ {
+ foreach ($inputDefinition as $key => $parameter) {
+ if ($parameter->getName() === 'applicationUuid') {
+ $usageParts = explode(' ', $usage);
+ $usageParts[$key] = "myapp";
+ $usage = implode(' ', $usageParts);
+ $command->addUsage($usage);
+ }
+ if ($parameter->getName() === 'environmentId') {
+ $usageParts = explode(' ', $usage);
+ $usageParts[$key] = "myapp.dev";
+ $usage = implode(' ', $usageParts);
+ $command->addUsage($usage);
+ }
+ }
}
- }
- private function addAliasParameterDescriptions(mixed &$paramDefinition): void {
- if ($paramDefinition['name'] === 'applicationUuid') {
- $paramDefinition['description'] .= ' You may also use an application alias or omit the argument if you run the command in a linked directory.';
+ private function addAliasParameterDescriptions(mixed &$paramDefinition): void
+ {
+ if ($paramDefinition['name'] === 'applicationUuid') {
+ $paramDefinition['description'] .= ' You may also use an application alias or omit the argument if you run the command in a linked directory.';
+ }
+ if ($paramDefinition['name'] === 'environmentId') {
+ $paramDefinition['description'] .= " You may also use an environment alias or UUID.";
+ }
}
- if ($paramDefinition['name'] === 'environmentId') {
- $paramDefinition['description'] .= " You may also use an environment alias or UUID.";
+
+ /**
+ * @return array
+ */
+ private function getRequestBodyFromParameterSchema(array $schema, array $acquiaCloudSpec): array
+ {
+ $requestBodyContent = $this->getRequestBodyContent($schema['requestBody']);
+ $requestBodySchema = $requestBodyContent['schema'];
+
+ // If this is a reference to the top level schema, go grab the referenced component.
+ if (array_key_exists('$ref', $requestBodySchema)) {
+ $parts = explode('/', $requestBodySchema['$ref']);
+ $paramKey = end($parts);
+ $requestBodySchema = $this->getParameterSchemaFromSpec($paramKey, $acquiaCloudSpec);
+ }
+
+ return $requestBodySchema;
}
- }
-
- /**
- * @return array
- */
- private function getRequestBodyFromParameterSchema(array $schema, array $acquiaCloudSpec): array {
- $requestBodyContent = $this->getRequestBodyContent($schema['requestBody']);
- $requestBodySchema = $requestBodyContent['schema'];
-
- // If this is a reference to the top level schema, go grab the referenced component.
- if (array_key_exists('$ref', $requestBodySchema)) {
- $parts = explode('/', $requestBodySchema['$ref']);
- $paramKey = end($parts);
- $requestBodySchema = $this->getParameterSchemaFromSpec($paramKey, $acquiaCloudSpec);
+
+ private function getPropertySpecFromRequestBodyParam(array $requestBodySchema, mixed $parameterDefinition): mixed
+ {
+ return $requestBodySchema['properties'][$parameterDefinition->getName()] ?? null;
}
- return $requestBodySchema;
- }
-
- private function getPropertySpecFromRequestBodyParam(array $requestBodySchema, mixed $parameterDefinition): mixed {
- return $requestBodySchema['properties'][$parameterDefinition->getName()] ?? NULL;
- }
-
- /**
- * @return array
- */
- protected static function getParameterRenameMap(): array {
- // Format should be ['original => new'].
- return [
- // @see api:environments:cron-create
- 'command' => 'cron_command',
- // @see api:environments:update.
- 'version' => 'lang_version',
- ];
- }
-
- public static function renameParameter(mixed $propKey): mixed {
- $parameterRenameMap = self::getParameterRenameMap();
- if (array_key_exists($propKey, $parameterRenameMap)) {
- $propKey = $parameterRenameMap[$propKey];
+ /**
+ * @return array
+ */
+ protected static function getParameterRenameMap(): array
+ {
+ // Format should be ['original => new'].
+ return [
+ // @see api:environments:cron-create
+ 'command' => 'cron_command',
+ // @see api:environments:update.
+ 'version' => 'lang_version',
+ ];
}
- return $propKey;
- }
- public static function restoreRenamedParameter(string $propKey): int|string {
- $parameterRenameMap = array_flip(self::getParameterRenameMap());
- if (array_key_exists($propKey, $parameterRenameMap)) {
- $propKey = $parameterRenameMap[$propKey];
+ public static function renameParameter(mixed $propKey): mixed
+ {
+ $parameterRenameMap = self::getParameterRenameMap();
+ if (array_key_exists($propKey, $parameterRenameMap)) {
+ $propKey = $parameterRenameMap[$propKey];
+ }
+ return $propKey;
}
- return $propKey;
- }
-
- /**
- * @return ApiListCommandBase[]
- */
- private function generateApiListCommands(array $apiCommands, string $commandPrefix, CommandFactoryInterface $commandFactory): array {
- $apiListCommands = [];
- foreach ($apiCommands as $apiCommand) {
- $commandNameParts = explode(':', $apiCommand->getName());
- if (count($commandNameParts) < 3) {
- continue;
- }
- $namespace = $commandNameParts[1];
- if (!array_key_exists($namespace, $apiListCommands)) {
- /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */
- $command = $commandFactory->createListCommand();
- $name = $commandPrefix . ':' . $namespace;
- $command->setName($name);
- $command->setNamespace($name);
- $command->setAliases([]);
- $command->setDescription("List all API commands for the {$namespace} resource");
- $apiListCommands[$name] = $command;
- }
+
+ public static function restoreRenamedParameter(string $propKey): int|string
+ {
+ $parameterRenameMap = array_flip(self::getParameterRenameMap());
+ if (array_key_exists($propKey, $parameterRenameMap)) {
+ $propKey = $parameterRenameMap[$propKey];
+ }
+ return $propKey;
}
- return $apiListCommands;
- }
-
- /**
- * @param array $requestBody
- * @return array
- */
- private function getRequestBodyContent(array $requestBody): array {
- $content = $requestBody['content'];
- $knownContentTypes = [
- 'application/hal+json',
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data',
- ];
- foreach ($knownContentTypes as $contentType) {
- if (array_key_exists($contentType, $content)) {
- return $content[$contentType];
- }
+
+ /**
+ * @return ApiListCommandBase[]
+ */
+ private function generateApiListCommands(array $apiCommands, string $commandPrefix, CommandFactoryInterface $commandFactory): array
+ {
+ $apiListCommands = [];
+ foreach ($apiCommands as $apiCommand) {
+ $commandNameParts = explode(':', $apiCommand->getName());
+ if (count($commandNameParts) < 3) {
+ continue;
+ }
+ $namespace = $commandNameParts[1];
+ if (!array_key_exists($namespace, $apiListCommands)) {
+ /** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */
+ $command = $commandFactory->createListCommand();
+ $name = $commandPrefix . ':' . $namespace;
+ $command->setName($name);
+ $command->setNamespace($name);
+ $command->setAliases([]);
+ $command->setDescription("List all API commands for the {$namespace} resource");
+ $apiListCommands[$name] = $command;
+ }
+ }
+ return $apiListCommands;
}
- throw new AcquiaCliException("requestBody content doesn't match any known schema");
- }
+ /**
+ * @param array $requestBody
+ * @return array
+ */
+ private function getRequestBodyContent(array $requestBody): array
+ {
+ $content = $requestBody['content'];
+ $knownContentTypes = [
+ 'application/hal+json',
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data',
+ ];
+ foreach ($knownContentTypes as $contentType) {
+ if (array_key_exists($contentType, $content)) {
+ return $content[$contentType];
+ }
+ }
+ throw new AcquiaCliException("requestBody content doesn't match any known schema");
+ }
}
diff --git a/src/Command/Api/ApiListCommand.php b/src/Command/Api/ApiListCommand.php
index 3f71a8c19..6b0d4c007 100644
--- a/src/Command/Api/ApiListCommand.php
+++ b/src/Command/Api/ApiListCommand.php
@@ -1,6 +1,6 @@
namespace = $namespace;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $commands = $this->getApplication()->all();
- foreach ($commands as $command) {
- if ($command->getName() !== $this->namespace
- && str_contains($command->getName(), $this->namespace . ':')
- // This is a lazy way to exclude api:base and acsf:base.
- && $command->getDescription()
- ) {
- $command->setHidden(FALSE);
- }
- else {
- $command->setHidden();
- }
- }
-
- $command = $this->getApplication()->find('list');
- $arguments = [
- 'command' => 'list',
- 'namespace' => 'api',
- ];
- $listInput = new ArrayInput($arguments);
+class ApiListCommandBase extends CommandBase
+{
+ protected string $namespace;
- return $command->run($listInput, $output);
- }
+ public function setNamespace(string $namespace): void
+ {
+ $this->namespace = $namespace;
+ }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $commands = $this->getApplication()->all();
+ foreach ($commands as $command) {
+ if (
+ $command->getName() !== $this->namespace
+ && str_contains($command->getName(), $this->namespace . ':')
+ // This is a lazy way to exclude api:base and acsf:base.
+ && $command->getDescription()
+ ) {
+ $command->setHidden(false);
+ } else {
+ $command->setHidden();
+ }
+ }
+
+ $command = $this->getApplication()->find('list');
+ $arguments = [
+ 'command' => 'list',
+ 'namespace' => 'api',
+ ];
+ $listInput = new ArrayInput($arguments);
+
+ return $command->run($listInput, $output);
+ }
}
diff --git a/src/Command/App/AppOpenCommand.php b/src/Command/App/AppOpenCommand.php
index 7d78ec612..386154d3b 100644
--- a/src/Command/App/AppOpenCommand.php
+++ b/src/Command/App/AppOpenCommand.php
@@ -1,6 +1,6 @@
acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- if (!$this->localMachineHelper->isBrowserAvailable()) {
- throw new AcquiaCliException('No browser is available on this machine');
+final class AppOpenCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->acceptApplicationUuid();
}
- $applicationUuid = $this->determineCloudApplication();
- $this->localMachineHelper->startBrowser('https://cloud.acquia.com/a/applications/' . $applicationUuid);
- return Command::SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if (!$this->localMachineHelper->isBrowserAvailable()) {
+ throw new AcquiaCliException('No browser is available on this machine');
+ }
+ $applicationUuid = $this->determineCloudApplication();
+ $this->localMachineHelper->startBrowser('https://cloud.acquia.com/a/applications/' . $applicationUuid);
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/App/AppVcsInfo.php b/src/Command/App/AppVcsInfo.php
index 768f72522..85250d224 100644
--- a/src/Command/App/AppVcsInfo.php
+++ b/src/Command/App/AppVcsInfo.php
@@ -1,6 +1,6 @@
addOption('deployed', NULL, InputOption::VALUE_OPTIONAL, 'Show only deployed branches and tags')
- ->addUsage('[] --deployed');
- $this->acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $applicationUuid = $this->determineCloudApplication();
+class AppVcsInfo extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('deployed', null, InputOption::VALUE_OPTIONAL, 'Show only deployed branches and tags')
+ ->addUsage('[] --deployed');
+ $this->acceptApplicationUuid();
+ }
- $cloudApiClient = $this->cloudApiClientService->getClient();
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $applicationUuid = $this->determineCloudApplication();
- $envResource = new Environments($cloudApiClient);
- $environments = $envResource->getAll($applicationUuid);
+ $cloudApiClient = $this->cloudApiClientService->getClient();
- if (!$environments->count()) {
- throw new AcquiaCliException('There are no environments available with this application.');
- }
+ $envResource = new Environments($cloudApiClient);
+ $environments = $envResource->getAll($applicationUuid);
- // To show branches and tags which are deployed only.
- $showDeployedVcsOnly = $input->hasParameterOption('--deployed');
+ if (!$environments->count()) {
+ throw new AcquiaCliException('There are no environments available with this application.');
+ }
- // Prepare list of all deployed VCS paths.
- $deployedVcs = [];
- foreach ($environments as $environment) {
- if (isset($environment->vcs->path)) {
- $deployedVcs[$environment->vcs->path] = $environment->label;
- }
- }
+ // To show branches and tags which are deployed only.
+ $showDeployedVcsOnly = $input->hasParameterOption('--deployed');
- // If only to show the deployed VCS but no VCS is deployed.
- if ($showDeployedVcsOnly && empty($deployedVcs)) {
- throw new AcquiaCliException('No branch or tag is deployed on any of the environment of this application.');
- }
+ // Prepare list of all deployed VCS paths.
+ $deployedVcs = [];
+ foreach ($environments as $environment) {
+ if (isset($environment->vcs->path)) {
+ $deployedVcs[$environment->vcs->path] = $environment->label;
+ }
+ }
- $applicationCodeResource = new Code($cloudApiClient);
- $allBranchesAndTags = $applicationCodeResource->getAll($applicationUuid);
+ // If only to show the deployed VCS but no VCS is deployed.
+ if ($showDeployedVcsOnly && empty($deployedVcs)) {
+ throw new AcquiaCliException('No branch or tag is deployed on any of the environment of this application.');
+ }
- if (!$allBranchesAndTags->count()) {
- throw new AcquiaCliException('No branch or tag is available with this application.');
- }
+ $applicationCodeResource = new Code($cloudApiClient);
+ $allBranchesAndTags = $applicationCodeResource->getAll($applicationUuid);
- $nonDeployedVcs = [];
- // Show both deployed and non-deployed VCS.
- if (!$showDeployedVcsOnly) {
- // Prepare list of all non-deployed VCS paths.
- foreach ($allBranchesAndTags as $branchTag) {
- if (!isset($deployedVcs[$branchTag->name])) {
- $nonDeployedVcs[$branchTag->name] = $branchTag->name;
+ if (!$allBranchesAndTags->count()) {
+ throw new AcquiaCliException('No branch or tag is available with this application.');
}
- }
- }
- // To show the deployed VCS paths on top.
- $allVcs = array_merge($deployedVcs, $nonDeployedVcs);
- $headers = ['Branch / Tag Name', 'Deployed', 'Deployed Environment'];
- $table = new Table($output);
- $table->setHeaders($headers);
- $table->setHeaderTitle('Status of Branches and Tags of the Application');
- foreach ($allVcs as $vscPath => $env) {
- $table->addRow([
- $vscPath,
- // If VCS and env name is not same, it means it is deployed.
- $vscPath !== $env ? 'Yes' : 'No',
- // If VCS and env name is same, it means it is deployed.
- $vscPath !== $env ? $env : 'None',
- ]);
- }
+ $nonDeployedVcs = [];
+ // Show both deployed and non-deployed VCS.
+ if (!$showDeployedVcsOnly) {
+ // Prepare list of all non-deployed VCS paths.
+ foreach ($allBranchesAndTags as $branchTag) {
+ if (!isset($deployedVcs[$branchTag->name])) {
+ $nonDeployedVcs[$branchTag->name] = $branchTag->name;
+ }
+ }
+ }
- $table->render();
- $this->io->newLine();
+ // To show the deployed VCS paths on top.
+ $allVcs = array_merge($deployedVcs, $nonDeployedVcs);
+ $headers = ['Branch / Tag Name', 'Deployed', 'Deployed Environment'];
+ $table = new Table($output);
+ $table->setHeaders($headers);
+ $table->setHeaderTitle('Status of Branches and Tags of the Application');
+ foreach ($allVcs as $vscPath => $env) {
+ $table->addRow([
+ $vscPath,
+ // If VCS and env name is not same, it means it is deployed.
+ $vscPath !== $env ? 'Yes' : 'No',
+ // If VCS and env name is same, it means it is deployed.
+ $vscPath !== $env ? $env : 'None',
+ ]);
+ }
- return self::SUCCESS;
- }
+ $table->render();
+ $this->io->newLine();
+ return self::SUCCESS;
+ }
}
diff --git a/src/Command/App/From/Composer/ProjectBuilder.php b/src/Command/App/From/Composer/ProjectBuilder.php
index b7f56558e..71a17b6a9 100644
--- a/src/Command/App/From/Composer/ProjectBuilder.php
+++ b/src/Command/App/From/Composer/ProjectBuilder.php
@@ -1,6 +1,6 @@
configuration = $configuration;
- $this->resolver = $recommendation_resolver;
- $this->siteInspector = $site_inspector;
- }
-
- /**
- * Gets an array representing a D9+ composer.json file for the current site.
- *
- * @return array
- * An array that can be encoded as JSON and written to a file. Calling
- * `composer install` in the same directory as that file should yield a new
- * Drupal project with Drupal 9+ installed, in addition to the Acquia
- * Migrate module, and some of all of the D9 replacements for the current
- * site's Drupal 7 modules.
- */
- public function buildProject(): array {
- $modules_to_install = [];
- $recommendations = [];
- $composer_json = $this->configuration->getRootPackageDefinition();
-
- // Add recommended dependencies and patches, if any.
- foreach ($this->resolver->getRecommendations() as $recommendation) {
- assert($recommendation instanceof RecommendationInterface);
- if ($recommendation instanceof NormalizableInterface) {
- $recommendations[] = $recommendation->normalize();
- }
- if ($recommendation instanceof AbandonmentRecommendation) {
- continue;
- }
- $recommended_package_name = $recommendation->getPackageName();
- // Special case: to guarantee that a valid and installable `composer.json`
- // file is generated, `$composer_json` is first populated with valid
- // `require`s based on `config.json`:
- // - `drupal/core-composer-scaffold`
- // - `drupal/core-project-message`
- // - `drupal/core-recommended`
- // When `recommendations.json` is unreachable or invalid, the versions
- // specified in `config.json` are what end up in the `composer.json` file.
- // Since without a `recommendations.json` file no patches can be applied,
- // this guarantees a viable Drupal 9-based `composer.json`.
- // However, when a valid `recommendations.json` is available, then that
- // default version that `$composer_json` was initialized with should be
- // overwritten by the recommended `drupal/core` version in
- // `recommendations.json`, because the patches associated with Drupal core
- // may only apply to a particular version.
- // Because the `drupal/core-*` package versions have sensible defaults
- // from `config.json` and are overwritten if and only if a valid
- // `recommendations.json` is available, we can guarantee a viable
- // `composer.json` file.
- if ($recommended_package_name === 'drupal/core') {
- $core_version_constraint = $recommendation->getVersionConstraint();
- if ($core_version_constraint !== '*') {
- $composer_json['require']['drupal/core-composer-scaffold'] = $core_version_constraint;
- $composer_json['require']['drupal/core-project-message'] = $core_version_constraint;
- $composer_json['require']['drupal/core-recommended'] = $core_version_constraint;
- }
- }
- else {
- $composer_json['require'][$recommended_package_name] = $recommendation->getVersionConstraint();
- }
- if ($recommendation->hasPatches()) {
- $composer_json['extra']['patches'][$recommended_package_name] = $recommendation->getPatches();
- }
- if ($recommendation->isVetted() && $recommendation->hasModulesToInstall()) {
- array_push($modules_to_install, ...$recommendation->getModulesToInstall());
- }
+final class ProjectBuilder
+{
+ /**
+ * The current configuration.
+ */
+ protected Configuration $configuration;
+
+ /**
+ * The recommendation resolver.
+ */
+ protected Resolver $resolver;
+
+ /**
+ * The site inspector.
+ */
+ protected SiteInspectorInterface $siteInspector;
+
+ /**
+ * ProjectBuilder constructor.
+ *
+ * @param \Acquia\Cli\Command\App\From\Configuration $configuration
+ * A configuration object.
+ * @param \Acquia\Cli\Command\App\From\Recommendation\Resolver $recommendation_resolver
+ * A recommendation resolver.
+ * @param \Acquia\Cli\Command\App\From\SourceSite\SiteInspectorInterface $site_inspector
+ * A site inspector.
+ */
+ public function __construct(Configuration $configuration, Resolver $recommendation_resolver, SiteInspectorInterface $site_inspector)
+ {
+ $this->configuration = $configuration;
+ $this->resolver = $recommendation_resolver;
+ $this->siteInspector = $site_inspector;
}
- // Multiple recommendations may ask the same module to get installed.
- $modules_to_install = array_unique($modules_to_install);
-
- // Sort the dependencies and patches by package name.
- sort($modules_to_install);
- if (isset($composer_json['require'])) {
- ksort($composer_json['require']);
- }
- if (isset($composer_json['extra']['patches'])) {
- ksort($composer_json['extra']['patches']);
- }
+ /**
+ * Gets an array representing a D9+ composer.json file for the current site.
+ *
+ * @return array
+ * An array that can be encoded as JSON and written to a file. Calling
+ * `composer install` in the same directory as that file should yield a new
+ * Drupal project with Drupal 9+ installed, in addition to the Acquia
+ * Migrate module, and some of all of the D9 replacements for the current
+ * site's Drupal 7 modules.
+ */
+ public function buildProject(): array
+ {
+ $modules_to_install = [];
+ $recommendations = [];
+ $composer_json = $this->configuration->getRootPackageDefinition();
+
+ // Add recommended dependencies and patches, if any.
+ foreach ($this->resolver->getRecommendations() as $recommendation) {
+ assert($recommendation instanceof RecommendationInterface);
+ if ($recommendation instanceof NormalizableInterface) {
+ $recommendations[] = $recommendation->normalize();
+ }
+ if ($recommendation instanceof AbandonmentRecommendation) {
+ continue;
+ }
+ $recommended_package_name = $recommendation->getPackageName();
+ // Special case: to guarantee that a valid and installable `composer.json`
+ // file is generated, `$composer_json` is first populated with valid
+ // `require`s based on `config.json`:
+ // - `drupal/core-composer-scaffold`
+ // - `drupal/core-project-message`
+ // - `drupal/core-recommended`
+ // When `recommendations.json` is unreachable or invalid, the versions
+ // specified in `config.json` are what end up in the `composer.json` file.
+ // Since without a `recommendations.json` file no patches can be applied,
+ // this guarantees a viable Drupal 9-based `composer.json`.
+ // However, when a valid `recommendations.json` is available, then that
+ // default version that `$composer_json` was initialized with should be
+ // overwritten by the recommended `drupal/core` version in
+ // `recommendations.json`, because the patches associated with Drupal core
+ // may only apply to a particular version.
+ // Because the `drupal/core-*` package versions have sensible defaults
+ // from `config.json` and are overwritten if and only if a valid
+ // `recommendations.json` is available, we can guarantee a viable
+ // `composer.json` file.
+ if ($recommended_package_name === 'drupal/core') {
+ $core_version_constraint = $recommendation->getVersionConstraint();
+ if ($core_version_constraint !== '*') {
+ $composer_json['require']['drupal/core-composer-scaffold'] = $core_version_constraint;
+ $composer_json['require']['drupal/core-project-message'] = $core_version_constraint;
+ $composer_json['require']['drupal/core-recommended'] = $core_version_constraint;
+ }
+ } else {
+ $composer_json['require'][$recommended_package_name] = $recommendation->getVersionConstraint();
+ }
+ if ($recommendation->hasPatches()) {
+ $composer_json['extra']['patches'][$recommended_package_name] = $recommendation->getPatches();
+ }
+ if ($recommendation->isVetted() && $recommendation->hasModulesToInstall()) {
+ array_push($modules_to_install, ...$recommendation->getModulesToInstall());
+ }
+ }
- $source_modules = array_values(array_map(function (ExtensionInterface $module) {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- return [
- 'name' => $module->getName(),
- 'humanName' => $module->getHumanName(),
- 'version' => $module->getVersion(),
- ];
- // phpcs:enable
- }, $this->siteInspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED)));
- $module_names = array_column($source_modules, 'name');
- array_multisort($module_names, SORT_STRING, $source_modules);
+ // Multiple recommendations may ask the same module to get installed.
+ $modules_to_install = array_unique($modules_to_install);
- $recommendation_ids = array_column($recommendations, 'id');
- array_multisort($recommendation_ids, SORT_STRING, $recommendations);
+ // Sort the dependencies and patches by package name.
+ sort($modules_to_install);
+ if (isset($composer_json['require'])) {
+ ksort($composer_json['require']);
+ }
+ if (isset($composer_json['extra']['patches'])) {
+ ksort($composer_json['extra']['patches']);
+ }
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- return [
- 'installModules' => $modules_to_install,
- 'filePaths' => [
+ $source_modules = array_values(array_map(function (ExtensionInterface $module) {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ return [
+ 'name' => $module->getName(),
+ 'humanName' => $module->getHumanName(),
+ 'version' => $module->getVersion(),
+ ];
+ // phpcs:enable
+ }, $this->siteInspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED)));
+ $module_names = array_column($source_modules, 'name');
+ array_multisort($module_names, SORT_STRING, $source_modules);
+
+ $recommendation_ids = array_column($recommendations, 'id');
+ array_multisort($recommendation_ids, SORT_STRING, $recommendations);
+
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ return [
+ 'installModules' => $modules_to_install,
+ 'filePaths' => [
'public' => $this->siteInspector->getPublicFilePath(),
'private' => $this->siteInspector->getPrivateFilePath(),
- ],
- 'sourceModules' => $source_modules,
- 'recommendations' => $recommendations,
- 'rootPackageDefinition' => $composer_json,
- ];
- // phpcs:enable
- }
-
+ ],
+ 'sourceModules' => $source_modules,
+ 'recommendations' => $recommendations,
+ 'rootPackageDefinition' => $composer_json,
+ ];
+ // phpcs:enable
+ }
}
diff --git a/src/Command/App/From/Configuration.php b/src/Command/App/From/Configuration.php
index eb645875f..bacf660d0 100644
--- a/src/Command/App/From/Configuration.php
+++ b/src/Command/App/From/Configuration.php
@@ -1,6 +1,6 @@
- */
- protected array $array;
-
- /**
- * Configuration constructor.
- *
- * @param array $config
- * An array of configuration, usually parsed from a configuration file.
- */
- protected function __construct(array $config) {
- $this->array = static::schema([
- 'rootPackageDefinition' => 'is_array',
- ])($config);
- }
-
- /**
- * Creates a configuration object from configuration given as a PHP resource.
- *
- * The given PHP resource is usually obtained by calling fopen($location).
- *
- * @param resource $configuration_resource
- * Configuration to be parse; given as a PHP resource.
- * @return \Acquia\Cli\Command\App\From\Configuration
- * A new configuration object.
- */
- public static function createFromResource($configuration_resource): Configuration {
- return new static(static::parseJsonResource($configuration_resource));
- }
-
- /**
- * Gets an basic root composer package definition for a Drupal 9+ project.
- *
- * @return array
- * An array representing a root composer package definition. From this
- * starting point, additional dependencies and metadata can be added until
- * an acceptable project is defined for migrating a source site to Drupal
- * 9+.
- */
- public function getRootPackageDefinition(): array {
- return $this->array['rootPackageDefinition'];
- }
+final class Configuration
+{
+ use ArrayValidationTrait;
+ use JsonResourceParserTrait;
+ /**
+ * The current configuration, usually parsed from a file.
+ *
+ * @var array
+ */
+ protected array $array;
+
+ /**
+ * Configuration constructor.
+ *
+ * @param array $config
+ * An array of configuration, usually parsed from a configuration file.
+ */
+ protected function __construct(array $config)
+ {
+ $this->array = static::schema([
+ 'rootPackageDefinition' => 'is_array',
+ ])($config);
+ }
+
+ /**
+ * Creates a configuration object from configuration given as a PHP resource.
+ *
+ * The given PHP resource is usually obtained by calling fopen($location).
+ *
+ * @param resource $configuration_resource
+ * Configuration to be parse; given as a PHP resource.
+ * @return \Acquia\Cli\Command\App\From\Configuration
+ * A new configuration object.
+ */
+ public static function createFromResource($configuration_resource): Configuration
+ {
+ return new static(static::parseJsonResource($configuration_resource));
+ }
+
+ /**
+ * Gets an basic root composer package definition for a Drupal 9+ project.
+ *
+ * @return array
+ * An array representing a root composer package definition. From this
+ * starting point, additional dependencies and metadata can be added until
+ * an acceptable project is defined for migrating a source site to Drupal
+ * 9+.
+ */
+ public function getRootPackageDefinition(): array
+ {
+ return $this->array['rootPackageDefinition'];
+ }
}
diff --git a/src/Command/App/From/JsonResourceParserTrait.php b/src/Command/App/From/JsonResourceParserTrait.php
index f01f47e81..4311fd20b 100644
--- a/src/Command/App/From/JsonResourceParserTrait.php
+++ b/src/Command/App/From/JsonResourceParserTrait.php
@@ -1,26 +1,26 @@
- */
- protected array $definition;
-
- /**
- * An array of extensions to which this recommendation applied.
- *
- * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
- */
- protected array $appliedTo = [];
-
- /**
- * AbandonmentRecommendation constructor.
- *
- * @param \Closure $extension_evaluator
- * An anonymous function that determines if this recommendation applies to
- * an extension.
- * @param array $definition
- * The original decoded definition.
- */
- protected function __construct(Closure $extension_evaluator, array $definition) {
- $this->evaluateExtension = $extension_evaluator;
- $this->definition = $definition;
- }
-
- /**
- * Creates a new recommendation.
- *
- * @param mixed $definition
- * A static recommendation definition. This must be an array. However, other
- * value types are accepted because this method performs validation on the
- * given value.
- * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface
- * A new AbandonmentRecommendation object if the given definition is valid or
- * a new NoRecommendation object otherwise.
- */
- public static function createFromDefinition(mixed $definition): RecommendationInterface {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- $validator = static::schema([
- 'package' => 'is_null',
- 'note' => 'is_string',
- 'replaces' => static::schema([
+class AbandonmentRecommendation implements RecommendationInterface, NormalizableInterface
+{
+ use ArrayValidationTrait;
+
+ /**
+ * An anonymous function that determines if this recommendation is applicable.
+ */
+ protected \Closure $evaluateExtension;
+
+ /**
+ * The original decoded definition.
+ *
+ * @var array
+ */
+ protected array $definition;
+
+ /**
+ * An array of extensions to which this recommendation applied.
+ *
+ * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
+ */
+ protected array $appliedTo = [];
+
+ /**
+ * AbandonmentRecommendation constructor.
+ *
+ * @param \Closure $extension_evaluator
+ * An anonymous function that determines if this recommendation applies to
+ * an extension.
+ * @param array $definition
+ * The original decoded definition.
+ */
+ protected function __construct(Closure $extension_evaluator, array $definition)
+ {
+ $this->evaluateExtension = $extension_evaluator;
+ $this->definition = $definition;
+ }
+
+ /**
+ * Creates a new recommendation.
+ *
+ * @param mixed $definition
+ * A static recommendation definition. This must be an array. However, other
+ * value types are accepted because this method performs validation on the
+ * given value.
+ * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface
+ * A new AbandonmentRecommendation object if the given definition is valid or
+ * a new NoRecommendation object otherwise.
+ */
+ public static function createFromDefinition(mixed $definition): RecommendationInterface
+ {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ $validator = static::schema([
+ 'package' => 'is_null',
+ 'note' => 'is_string',
+ 'replaces' => static::schema([
'name' => 'is_string',
- ]),
- 'vetted' => 'is_bool',
- ]);
- // phpcs:enable
- try {
- $validated = $validator($definition);
+ ]),
+ 'vetted' => 'is_bool',
+ ]);
+ // phpcs:enable
+ try {
+ $validated = $validator($definition);
+ } catch (Exception $e) {
+ // Under any circumstance where the given recommendation configuration is
+ // invalid, we still want the rest of the script to proceed. I.e. it's
+ // better to produce a valid composer.json with a missing recommendation
+ // than to fail to create one at all.
+ return new NoRecommendation();
+ }
+ return new AbandonmentRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool {
+ return $extension->getName() === $validated['replaces']['name'];
+ }), $validated);
+ }
+
+ public function applies(ExtensionInterface $extension): bool
+ {
+ if (($this->evaluateExtension)($extension)) {
+ array_push($this->appliedTo, $extension);
+ return true;
+ }
+ return false;
+ }
+
+ public function getPackageName(): string
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a % class instance.', __FUNCTION__, __CLASS__));
+ }
+
+ public function getVersionConstraint(): string
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
+ }
+
+ public function hasModulesToInstall(): bool
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
}
- catch (Exception $e) {
- // Under any circumstance where the given recommendation configuration is
- // invalid, we still want the rest of the script to proceed. I.e. it's
- // better to produce a valid composer.json with a missing recommendation
- // than to fail to create one at all.
- return new NoRecommendation();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getModulesToInstall(): array
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
+ }
+
+ public function hasPatches(): bool
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
+ }
+
+ public function isVetted(): bool
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
}
- return new AbandonmentRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool {
- return $extension->getName() === $validated['replaces']['name'];
- }), $validated);
- }
-
- public function applies(ExtensionInterface $extension): bool {
- if (($this->evaluateExtension)($extension)) {
- array_push($this->appliedTo, $extension);
- return TRUE;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPatches(): array
+ {
+ throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
}
- return FALSE;
- }
-
- public function getPackageName(): string {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a % class instance.', __FUNCTION__, __CLASS__));
- }
-
- public function getVersionConstraint(): string {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
- }
-
- public function hasModulesToInstall(): bool {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
- }
-
- /**
- * {@inheritdoc}
- */
- public function getModulesToInstall(): array {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
- }
-
- public function hasPatches(): bool {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
- }
-
- public function isVetted(): bool {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
- }
-
- /**
- * {@inheritdoc}
- */
- public function getPatches(): array {
- throw new LogicException(sprintf('It is nonsensical to call the %s() method on a %s class instance.', __FUNCTION__, __CLASS__));
- }
-
- /**
- * {@inheritDoc}
- */
- public function normalize(): array {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- $normalized = [
- 'type' => 'abandonmentRecommendation',
- 'id' => "abandon:{$this->definition['replaces']['name']}",
- 'attributes' => [
+
+ /**
+ * {@inheritDoc}
+ */
+ public function normalize(): array
+ {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ $normalized = [
+ 'type' => 'abandonmentRecommendation',
+ 'id' => "abandon:{$this->definition['replaces']['name']}",
+ 'attributes' => [
'note' => $this->definition['note'],
- ],
- ];
-
- $recommended_for = [
- 'data' => array_map(function (ExtensionInterface $extension) {
- return [
- 'type' => $extension->isModule() ? 'module' : 'theme',
- 'id' => $extension->getName(),
+ ],
];
- }, $this->appliedTo),
- ];
- // phpcs:enable
- if (!empty($recommended_for['data'])) {
- $normalized['relationships']['recommendedFor'] = $recommended_for;
- }
- return $normalized;
- }
+ $recommended_for = [
+ 'data' => array_map(function (ExtensionInterface $extension) {
+ return [
+ 'type' => $extension->isModule() ? 'module' : 'theme',
+ 'id' => $extension->getName(),
+ ];
+ }, $this->appliedTo),
+ ];
+ // phpcs:enable
+ if (!empty($recommended_for['data'])) {
+ $normalized['relationships']['recommendedFor'] = $recommended_for;
+ }
+ return $normalized;
+ }
}
diff --git a/src/Command/App/From/Recommendation/DefinedRecommendation.php b/src/Command/App/From/Recommendation/DefinedRecommendation.php
index 0abcd74aa..c858c9c69 100644
--- a/src/Command/App/From/Recommendation/DefinedRecommendation.php
+++ b/src/Command/App/From/Recommendation/DefinedRecommendation.php
@@ -1,6 +1,6 @@
- */
- protected array $patches;
-
- /**
- * An array of extensions to which this recommendation applied.
- *
- * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
- */
- protected array $appliedTo = [];
-
- /**
- * DefinedRecommendation constructor.
- *
- * @param \Closure $extension_evaluator
- * An anonymous function that determines if this recommendation applies to
- * an extension.
- * @param string $package_name
- * The name of the package recommended by this object.
- * @param string $version_constraint
- * The version constraint recommended by this object.
- * @param string[] $install
- * A list of recommended modules to install.
- * @param bool $vetted
- * Whether this is a vetted recommendation.
- * @param string $note
- * A note about the recommendation.
- * @param array $patches
- * An array of patch recommendations.
- */
- protected function __construct(Closure $extension_evaluator, string $package_name, string $version_constraint, array $install, bool $vetted, string $note, array $patches = []) {
- $this->evaluateExtension = $extension_evaluator;
- $this->packageName = $package_name;
- $this->versionConstraint = $version_constraint;
- $this->install = $install;
- $this->vetted = $vetted;
- $this->note = $note;
- $this->patches = $patches;
- }
-
- /**
- * Creates a new recommendation.
- *
- * @param mixed $definition
- * A static recommendation definition. This must be an array. However, other
- * value types are accepted because this method performs validation on the
- * given value.
- * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface
- * A new DefinedRecommendation object if the given definition is valid or
- * a new NoRecommendation object otherwise.
- */
- public static function createFromDefinition(mixed $definition): RecommendationInterface {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- $defaults = [
- 'universal' => FALSE,
- 'patches' => [],
- 'install' => [],
- 'vetted' => FALSE,
- 'note' => static::NOTE_PLACEHOLDER_STRING,
- ];
- $validate_if_universal_is_false = Closure::fromCallable(function ($context) {
- return $context['universal'] === FALSE;
- });
-
- if (is_array($definition) && array_key_exists('package', $definition) && is_null($definition['package'])) {
- return AbandonmentRecommendation::createFromDefinition($definition);
+class DefinedRecommendation implements RecommendationInterface, NormalizableInterface
+{
+ use ArrayValidationTrait;
+
+ /**
+ * Default note value.
+ *
+ * @const string
+ */
+ const NOTE_PLACEHOLDER_STRING = '%note%';
+
+ /**
+ * An anonymous function that determines if this recommendation is applicable.
+ */
+ protected \Closure $evaluateExtension;
+
+ /**
+ * The name of a recommended package.
+ */
+ protected string $packageName;
+
+ /**
+ * A recommended composer version constraint.
+ */
+ protected string $versionConstraint;
+
+ /**
+ * A list of recommended modules to install.
+ *
+ * @var string[]
+ */
+ protected array $install;
+
+ /**
+ * Whether this is a vetted recommendation.
+ */
+ protected bool $vetted;
+
+ /**
+ * A note about the recommendation.
+ */
+ protected string $note;
+
+ /**
+ * A list of recommended patches.
+ *
+ * The keys of the array should be descriptions of the patch contents and the
+ * values should be URLs where the recommended patch can be downloaded.
+ *
+ * @var array
+ */
+ protected array $patches;
+
+ /**
+ * An array of extensions to which this recommendation applied.
+ *
+ * @var \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
+ */
+ protected array $appliedTo = [];
+
+ /**
+ * DefinedRecommendation constructor.
+ *
+ * @param \Closure $extension_evaluator
+ * An anonymous function that determines if this recommendation applies to
+ * an extension.
+ * @param string $package_name
+ * The name of the package recommended by this object.
+ * @param string $version_constraint
+ * The version constraint recommended by this object.
+ * @param string[] $install
+ * A list of recommended modules to install.
+ * @param bool $vetted
+ * Whether this is a vetted recommendation.
+ * @param string $note
+ * A note about the recommendation.
+ * @param array $patches
+ * An array of patch recommendations.
+ */
+ protected function __construct(Closure $extension_evaluator, string $package_name, string $version_constraint, array $install, bool $vetted, string $note, array $patches = [])
+ {
+ $this->evaluateExtension = $extension_evaluator;
+ $this->packageName = $package_name;
+ $this->versionConstraint = $version_constraint;
+ $this->install = $install;
+ $this->vetted = $vetted;
+ $this->note = $note;
+ $this->patches = $patches;
}
- $validator = static::schema([
- 'universal' => 'is_bool',
- 'install' => static::listOf('is_string'),
- 'package' => 'is_string',
- 'constraint' => 'is_string',
- 'note' => 'is_string',
- 'replaces' => static::conditionalSchema([
+ /**
+ * Creates a new recommendation.
+ *
+ * @param mixed $definition
+ * A static recommendation definition. This must be an array. However, other
+ * value types are accepted because this method performs validation on the
+ * given value.
+ * @return \Acquia\Cli\Command\App\From\Recommendation\RecommendationInterface
+ * A new DefinedRecommendation object if the given definition is valid or
+ * a new NoRecommendation object otherwise.
+ */
+ public static function createFromDefinition(mixed $definition): RecommendationInterface
+ {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ $defaults = [
+ 'universal' => false,
+ 'patches' => [],
+ 'install' => [],
+ 'vetted' => false,
+ 'note' => static::NOTE_PLACEHOLDER_STRING,
+ ];
+ $validate_if_universal_is_false = Closure::fromCallable(function ($context) {
+ return $context['universal'] === false;
+ });
+
+ if (is_array($definition) && array_key_exists('package', $definition) && is_null($definition['package'])) {
+ return AbandonmentRecommendation::createFromDefinition($definition);
+ }
+
+ $validator = static::schema([
+ 'universal' => 'is_bool',
+ 'install' => static::listOf('is_string'),
+ 'package' => 'is_string',
+ 'constraint' => 'is_string',
+ 'note' => 'is_string',
+ 'replaces' => static::conditionalSchema([
'name' => 'is_string',
- ], $validate_if_universal_is_false),
- 'patches' => static::dictionaryOf('is_string'),
- 'vetted' => 'is_bool',
- ], $defaults);
- // phpcs:enable
- try {
- $validated = $validator($definition);
- }
- catch (Exception $e) {
- // Under any circumstance where the given recommendation configuration is
- // invalid, we still want the rest of the script to proceed. I.e. it's
- // better to produce a valid composer.json with a missing recommendation
- // than to fail to create one at all.
- return new NoRecommendation();
- }
- $package_name = $validated['package'];
- $version_constraint = $validated['constraint'];
- $install = $validated['install'];
- $patches = $validated['patches'];
- $vetted = $validated['vetted'];
- $note = $validated['note'];
- if ($validated['universal']) {
- return new UniversalRecommendation($package_name, $version_constraint, $install, $vetted, $note, $patches);
+ ], $validate_if_universal_is_false),
+ 'patches' => static::dictionaryOf('is_string'),
+ 'vetted' => 'is_bool',
+ ], $defaults);
+ // phpcs:enable
+ try {
+ $validated = $validator($definition);
+ } catch (Exception $e) {
+ // Under any circumstance where the given recommendation configuration is
+ // invalid, we still want the rest of the script to proceed. I.e. it's
+ // better to produce a valid composer.json with a missing recommendation
+ // than to fail to create one at all.
+ return new NoRecommendation();
+ }
+ $package_name = $validated['package'];
+ $version_constraint = $validated['constraint'];
+ $install = $validated['install'];
+ $patches = $validated['patches'];
+ $vetted = $validated['vetted'];
+ $note = $validated['note'];
+ if ($validated['universal']) {
+ return new UniversalRecommendation($package_name, $version_constraint, $install, $vetted, $note, $patches);
+ }
+ return new DefinedRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool {
+ return $extension->getName() === $validated['replaces']['name'];
+ }), $package_name, $version_constraint, $install, $vetted, $note, $patches);
}
- return new DefinedRecommendation(Closure::fromCallable(function (ExtensionInterface $extension) use ($validated): bool {
- return $extension->getName() === $validated['replaces']['name'];
- }), $package_name, $version_constraint, $install, $vetted, $note, $patches);
- }
- public function applies(ExtensionInterface $extension): bool {
- if (($this->evaluateExtension)($extension)) {
- array_push($this->appliedTo, $extension);
- return TRUE;
+ public function applies(ExtensionInterface $extension): bool
+ {
+ if (($this->evaluateExtension)($extension)) {
+ array_push($this->appliedTo, $extension);
+ return true;
+ }
+ return false;
}
- return FALSE;
- }
- public function getPackageName(): string {
- return $this->packageName;
- }
+ public function getPackageName(): string
+ {
+ return $this->packageName;
+ }
- public function getVersionConstraint(): string {
- return $this->versionConstraint;
- }
+ public function getVersionConstraint(): string
+ {
+ return $this->versionConstraint;
+ }
- public function hasModulesToInstall(): bool {
- return !empty($this->install);
- }
+ public function hasModulesToInstall(): bool
+ {
+ return !empty($this->install);
+ }
- /**
- * {@inheritDoc}
- */
- public function getModulesToInstall(): array {
- return $this->install;
- }
+ /**
+ * {@inheritDoc}
+ */
+ public function getModulesToInstall(): array
+ {
+ return $this->install;
+ }
- public function isVetted(): bool {
- return $this->vetted;
- }
+ public function isVetted(): bool
+ {
+ return $this->vetted;
+ }
- public function hasPatches(): bool {
- return !empty($this->patches);
- }
+ public function hasPatches(): bool
+ {
+ return !empty($this->patches);
+ }
- /**
- * {@inheritDoc}
- */
- public function getPatches(): array {
- return $this->patches;
- }
+ /**
+ * {@inheritDoc}
+ */
+ public function getPatches(): array
+ {
+ return $this->patches;
+ }
- /**
- * {@inheritdoc}
- */
- public function normalize(): array {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- $normalized = [
- 'type' => 'packageRecommendation',
- 'id' => "{$this->packageName}:{$this->versionConstraint}",
- 'attributes' => [
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize(): array
+ {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ $normalized = [
+ 'type' => 'packageRecommendation',
+ 'id' => "{$this->packageName}:{$this->versionConstraint}",
+ 'attributes' => [
'requirePackage' => [
- 'name' => $this->packageName,
- 'versionConstraint' => $this->versionConstraint,
+ 'name' => $this->packageName,
+ 'versionConstraint' => $this->versionConstraint,
],
'installModules' => $this->install,
'vetted' => $this->vetted,
- ],
- ];
-
- if (!empty($this->note) && $this->note !== static::NOTE_PLACEHOLDER_STRING) {
- $normalized['attributes']['note'] = $this->note;
- }
-
- $recommended_for = [
- 'data' => array_map(function (ExtensionInterface $extension) {
- return [
- 'type' => $extension->isModule() ? 'module' : 'theme',
- 'id' => $extension->getName(),
+ ],
];
- }, $this->appliedTo),
- ];
- // phpcs:enable
- if (!empty($recommended_for['data'])) {
- $normalized['relationships']['recommendedFor'] = $recommended_for;
- }
- $links = array_reduce(array_keys($this->patches), function (array $links, string $patch_description) {
- $links['patch-file--' . md5($patch_description)] = [
- 'href' => $this->patches[$patch_description],
- 'rel' => 'https://github.com/acquia/acquia_migrate#link-rel-patch-file',
- 'title' => $patch_description,
- ];
- return $links;
- }, []);
- if (!empty($links)) {
- $normalized['links'] = $links;
+ if (!empty($this->note) && $this->note !== static::NOTE_PLACEHOLDER_STRING) {
+ $normalized['attributes']['note'] = $this->note;
+ }
+
+ $recommended_for = [
+ 'data' => array_map(function (ExtensionInterface $extension) {
+ return [
+ 'type' => $extension->isModule() ? 'module' : 'theme',
+ 'id' => $extension->getName(),
+ ];
+ }, $this->appliedTo),
+ ];
+ // phpcs:enable
+ if (!empty($recommended_for['data'])) {
+ $normalized['relationships']['recommendedFor'] = $recommended_for;
+ }
+
+ $links = array_reduce(array_keys($this->patches), function (array $links, string $patch_description) {
+ $links['patch-file--' . md5($patch_description)] = [
+ 'href' => $this->patches[$patch_description],
+ 'rel' => 'https://github.com/acquia/acquia_migrate#link-rel-patch-file',
+ 'title' => $patch_description,
+ ];
+ return $links;
+ }, []);
+ if (!empty($links)) {
+ $normalized['links'] = $links;
+ }
+
+ return $normalized;
}
-
- return $normalized;
- }
-
}
diff --git a/src/Command/App/From/Recommendation/NoRecommendation.php b/src/Command/App/From/Recommendation/NoRecommendation.php
index 3f1884a3d..a2f4625dd 100644
--- a/src/Command/App/From/Recommendation/NoRecommendation.php
+++ b/src/Command/App/From/Recommendation/NoRecommendation.php
@@ -1,6 +1,6 @@
- */
- public function normalize(): array;
-
+interface NormalizableInterface
+{
+ /**
+ * Normalizes an object into a single- or multi-dimensional array of scalars.
+ *
+ * @return array
+ */
+ public function normalize(): array;
}
diff --git a/src/Command/App/From/Recommendation/RecommendationInterface.php b/src/Command/App/From/Recommendation/RecommendationInterface.php
index 959c90734..e7343df35 100644
--- a/src/Command/App/From/Recommendation/RecommendationInterface.php
+++ b/src/Command/App/From/Recommendation/RecommendationInterface.php
@@ -1,6 +1,6 @@
- * An associative array whose keys are a description of the patch's contents
- * and whose values are URLs or relative paths to a patch file.
- */
- public function getPatches(): array;
+ /**
+ * Whether the recommendation contains patches or not.
+ *
+ * @return bool
+ * TRUE if the recommendation contains patches; FALSE otherwise.
+ */
+ public function hasPatches(): bool;
+ /**
+ * Gets an array of recommended patches for the recommended package.
+ *
+ * @return array
+ * An associative array whose keys are a description of the patch's contents
+ * and whose values are URLs or relative paths to a patch file.
+ */
+ public function getPatches(): array;
}
diff --git a/src/Command/App/From/Recommendation/Recommendations.php b/src/Command/App/From/Recommendation/Recommendations.php
index db964b051..90667a283 100644
--- a/src/Command/App/From/Recommendation/Recommendations.php
+++ b/src/Command/App/From/Recommendation/Recommendations.php
@@ -1,6 +1,6 @@
inspector = $inspector;
+ $this->universalRecommendations = new Recommendations([]);
+ $this->conditionalRecommendations = new Recommendations([]);
+ foreach ($recommendations as $recommendation) {
+ if ($recommendation instanceof UniversalRecommendation) {
+ $this->universalRecommendations->append($recommendation);
+ } else {
+ $this->conditionalRecommendations->append($recommendation);
+ }
+ }
+ }
- /**
- * Resolver constructor.
- *
- * @param \Acquia\Cli\Command\App\From\SourceSite\SiteInspectorInterface $inspector
- * A site inspector.
- * @param \Acquia\Cli\Command\App\From\Recommendation\Recommendations $recommendations
- * A set of defined recommendations. These are *all possible*
- * recommendations. It is the resolves job to narrow these down by using the
- * site inspector to retrieve information about the source site.
- */
- public function __construct(SiteInspectorInterface $inspector, Recommendations $recommendations) {
- $this->inspector = $inspector;
- $this->universalRecommendations = new Recommendations([]);
- $this->conditionalRecommendations = new Recommendations([]);
- foreach ($recommendations as $recommendation) {
- if ($recommendation instanceof UniversalRecommendation) {
- $this->universalRecommendations->append($recommendation);
- }
- else {
- $this->conditionalRecommendations->append($recommendation);
- }
+ /**
+ * Gets a recommendation for the given extension.
+ *
+ * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations
+ * A resolved suite of recommendations.
+ */
+ public function getRecommendations(): Recommendations
+ {
+ $enabled_modules = $this->inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_ENABLED | SiteInspectorInterface::FLAG_EXTENSION_MODULE);
+ return array_reduce($enabled_modules, function (Recommendations $recommendations, ExtensionInterface $extension) {
+ $resolutions = $this->getRecommendationsForExtension($extension);
+ foreach ($resolutions as $resolution) {
+ if (!$resolution instanceof NoRecommendation) {
+ $recommendations->append($resolution);
+ }
+ }
+ return $recommendations;
+ }, $this->universalRecommendations);
}
- }
- /**
- * Gets a recommendation for the given extension.
- *
- * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations
- * A resolved suite of recommendations.
- */
- public function getRecommendations(): Recommendations {
- $enabled_modules = $this->inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_ENABLED | SiteInspectorInterface::FLAG_EXTENSION_MODULE);
- return array_reduce($enabled_modules, function (Recommendations $recommendations, ExtensionInterface $extension) {
- $resolutions = $this->getRecommendationsForExtension($extension);
- foreach ($resolutions as $resolution) {
- if (!$resolution instanceof NoRecommendation) {
- $recommendations->append($resolution);
+ /**
+ * Gets a recommendation for the given extension.
+ *
+ * @param \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface $extension
+ * A Drupal 7 extension for which a package recommendation should be
+ * resolved.
+ * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations
+ * A resolved recommendation.
+ */
+ protected function getRecommendationsForExtension(ExtensionInterface $extension): Recommendations
+ {
+ $recommendations = new Recommendations();
+ foreach ($this->conditionalRecommendations as $recommendation) {
+ if ($recommendation->applies($extension)) {
+ $recommendations->append($recommendation);
+ }
}
- }
- return $recommendations;
- }, $this->universalRecommendations);
- }
-
- /**
- * Gets a recommendation for the given extension.
- *
- * @param \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface $extension
- * A Drupal 7 extension for which a package recommendation should be
- * resolved.
- * @return \Acquia\Cli\Command\App\From\Recommendation\Recommendations
- * A resolved recommendation.
- */
- protected function getRecommendationsForExtension(ExtensionInterface $extension): Recommendations {
- $recommendations = new Recommendations();
- foreach ($this->conditionalRecommendations as $recommendation) {
- if ($recommendation->applies($extension)) {
- $recommendations->append($recommendation);
- }
+ return $recommendations;
}
- return $recommendations;
- }
-
}
diff --git a/src/Command/App/From/Recommendation/UniversalRecommendation.php b/src/Command/App/From/Recommendation/UniversalRecommendation.php
index cb1760cc4..f102f679d 100644
--- a/src/Command/App/From/Recommendation/UniversalRecommendation.php
+++ b/src/Command/App/From/Recommendation/UniversalRecommendation.php
@@ -1,6 +1,6 @@
$value) {
- if (!$key_validator($index)) {
- throw new DomainException("The array key `$index` must be an integer or a string.");
- }
- elseif ($value_validator instanceof StructuredArrayValidator || $value_validator instanceof Closure) {
- $values[$index] = $value_validator($value);
- }
- elseif (!call_user_func_array($value_validator, [$value])) {
- throw new DomainException('Failed to validate value.');
- }
- }
- return $values;
- });
- }
+ /**
+ * Creates a validator for an associative array with arbitrary string as keys.
+ *
+ * @param callable $entry_validator
+ * A validator to apply to each entry in a validated array.
+ * @return \Closure
+ * A validation function.
+ */
+ protected static function dictionaryOf(callable $entry_validator): Closure
+ {
+ return static::arrayOf('is_string', $entry_validator);
+ }
+ /**
+ * Creates an arbitrarily keyed array validator.
+ *
+ * @param callable $key_validator
+ * A callable, either 'is_int' or 'is_string' to check against each key of
+ * the given array.
+ * @param callable $value_validator
+ * A callable to evaluate against each value in the given array.
+ * @return \Closure
+ * A validation function.
+ */
+ private static function arrayOf(callable $key_validator, callable $value_validator): \Closure
+ {
+ assert(in_array($key_validator, ['is_int', 'is_string'], true));
+ return Closure::fromCallable(function ($values) use ($key_validator, $value_validator) {
+ if (!is_array($values)) {
+ throw new DomainException('Validated value is not an array.');
+ }
+ foreach ($values as $index => $value) {
+ if (!$key_validator($index)) {
+ throw new DomainException("The array key `$index` must be an integer or a string.");
+ } elseif ($value_validator instanceof StructuredArrayValidator || $value_validator instanceof Closure) {
+ $values[$index] = $value_validator($value);
+ } elseif (!call_user_func_array($value_validator, [$value])) {
+ throw new DomainException('Failed to validate value.');
+ }
+ }
+ return $values;
+ });
+ }
}
diff --git a/src/Command/App/From/Safety/StructuredArrayValidator.php b/src/Command/App/From/Safety/StructuredArrayValidator.php
index 3e3d63a47..25bc4a7da 100644
--- a/src/Command/App/From/Safety/StructuredArrayValidator.php
+++ b/src/Command/App/From/Safety/StructuredArrayValidator.php
@@ -1,6 +1,6 @@
+ */
+ protected array $schema;
- /**
- * A schema definition for the array to be validated.
- *
- * @var array
- */
- protected array $schema;
+ /**
+ * A set of defaults for the array to be validated.
+ *
+ * @var array
+ */
+ protected array $defaults;
- /**
- * A set of defaults for the array to be validated.
- *
- * @var array
- */
- protected array $defaults;
+ /**
+ * Whether the schema is conditional or not.
+ *
+ * @var bool|callable
+ */
+ protected $conditional;
- /**
- * Whether the schema is conditional or not.
- *
- * @var bool|callable
- */
- protected $conditional;
-
- /**
- * ArrayValidator constructor.
- *
- * @param array $schema
- * A schema definition for the array to be validated.
- * @param array $defaults
- * A set of defaults for the array to be validated.
- * @param bool|\Closure $conditional
- * A callable or FALSE. See self::createChildValidator().
- */
- protected function __construct(array $schema, array $defaults, bool|\Closure $conditional) {
- assert(!isset($schema[static::KEYS_ARE_STRINGS]) || empty(array_diff_key($schema, array_flip([static::KEYS_ARE_STRINGS]))), 'A schema must contain either the KEYS_ARE_STRINGS constant or validations for specific array keys, but not both.');
- assert($conditional === FALSE || $conditional instanceof Closure);
- $this->schema = $schema;
- $this->defaults = $defaults;
- $this->conditional = $conditional;
- }
-
- /**
- * Creates a new ArrayValidator.
- *
- * @param array $schema
- * A schema definition for the array to be validated.
- * @param array $defaults
- * A set of defaults for the array to be validated.
- * @return static
- * A new Array validator.
- */
- public static function create(array $schema, array $defaults = []): static {
- return new static($schema, $defaults, FALSE);
- }
+ /**
+ * ArrayValidator constructor.
+ *
+ * @param array $schema
+ * A schema definition for the array to be validated.
+ * @param array $defaults
+ * A set of defaults for the array to be validated.
+ * @param bool|\Closure $conditional
+ * A callable or FALSE. See self::createChildValidator().
+ */
+ protected function __construct(array $schema, array $defaults, bool|\Closure $conditional)
+ {
+ assert(!isset($schema[static::KEYS_ARE_STRINGS]) || empty(array_diff_key($schema, array_flip([static::KEYS_ARE_STRINGS]))), 'A schema must contain either the KEYS_ARE_STRINGS constant or validations for specific array keys, but not both.');
+ assert($conditional === false || $conditional instanceof Closure);
+ $this->schema = $schema;
+ $this->defaults = $defaults;
+ $this->conditional = $conditional;
+ }
- /**
- * Creates a new ArrayValidator.
- *
- * Note: only child validators, that is, validators that are children of
- * another validator should be conditional.
- *
- * @param array $schema
- * A schema definition for the array to be validated.
- * @param \Closure $conditional
- * The function should be a function that receives the context array and
- * returns a bool. If TRUE, the validation will be applied, otherwise it
- * will be skipped and the value will be omitted from the final validated
- * array.
- * @param array $defaults
- * A set of defaults for the array to be validated.
- * @return static
- * A new Array validator.
- */
- public static function createConditionalValidator(array $schema, Closure $conditional, array $defaults = []): static {
- return new static($schema, $defaults, $conditional);
- }
+ /**
+ * Creates a new ArrayValidator.
+ *
+ * @param array $schema
+ * A schema definition for the array to be validated.
+ * @param array $defaults
+ * A set of defaults for the array to be validated.
+ * @return static
+ * A new Array validator.
+ */
+ public static function create(array $schema, array $defaults = []): static
+ {
+ return new static($schema, $defaults, false);
+ }
- /**
- * Validates and returns a validated array.
- *
- * @param mixed $arr
- * An array to validate.
- * @return array
- * If the given $arr is valid, the given $arr value. Array keys not defined
- * in the schema definition will be stripped from return value.
- */
- public function __invoke(mixed $arr): array {
- if (!is_array($arr)) {
- throw new DomainException('Validated value is not an array.');
+ /**
+ * Creates a new ArrayValidator.
+ *
+ * Note: only child validators, that is, validators that are children of
+ * another validator should be conditional.
+ *
+ * @param array $schema
+ * A schema definition for the array to be validated.
+ * @param \Closure $conditional
+ * The function should be a function that receives the context array and
+ * returns a bool. If TRUE, the validation will be applied, otherwise it
+ * will be skipped and the value will be omitted from the final validated
+ * array.
+ * @param array $defaults
+ * A set of defaults for the array to be validated.
+ * @return static
+ * A new Array validator.
+ */
+ public static function createConditionalValidator(array $schema, Closure $conditional, array $defaults = []): static
+ {
+ return new static($schema, $defaults, $conditional);
}
- $arr += $this->defaults;
- // The schema must define expected keys, so validate each key accordingly.
- foreach ($this->schema as $schema_key => $validation) {
- // Validate in every case except where the validation is conditional and
- // the condition does *not* evaluate to TRUE.
- $should_validate = !$validation instanceof self || !$validation->isConditional() || ($validation->conditional)($arr);
- if ($should_validate && !array_key_exists($schema_key, $arr)) {
- throw new DomainException("Missing required key: $schema_key");
- }
- else {
- // If the validation does not apply, omit the value from the validated
- // return value.
- if ($validation instanceof self && !$should_validate) {
- unset($arr[$schema_key]);
+
+ /**
+ * Validates and returns a validated array.
+ *
+ * @param mixed $arr
+ * An array to validate.
+ * @return array
+ * If the given $arr is valid, the given $arr value. Array keys not defined
+ * in the schema definition will be stripped from return value.
+ */
+ public function __invoke(mixed $arr): array
+ {
+ if (!is_array($arr)) {
+ throw new DomainException('Validated value is not an array.');
}
- else {
- if ($validation instanceof self || $validation instanceof Closure) {
- $arr[$schema_key] = $validation($arr[$schema_key]);
- }
- elseif (!call_user_func_array($validation, [$arr[$schema_key]])) {
- throw new DomainException('Failed to validate value.');
- }
+ $arr += $this->defaults;
+ // The schema must define expected keys, so validate each key accordingly.
+ foreach ($this->schema as $schema_key => $validation) {
+ // Validate in every case except where the validation is conditional and
+ // the condition does *not* evaluate to TRUE.
+ $should_validate = !$validation instanceof self || !$validation->isConditional() || ($validation->conditional)($arr);
+ if ($should_validate && !array_key_exists($schema_key, $arr)) {
+ throw new DomainException("Missing required key: $schema_key");
+ } else {
+ // If the validation does not apply, omit the value from the validated
+ // return value.
+ if ($validation instanceof self && !$should_validate) {
+ unset($arr[$schema_key]);
+ } else {
+ if ($validation instanceof self || $validation instanceof Closure) {
+ $arr[$schema_key] = $validation($arr[$schema_key]);
+ } elseif (!call_user_func_array($validation, [$arr[$schema_key]])) {
+ throw new DomainException('Failed to validate value.');
+ }
+ }
+ }
}
- }
+ return array_intersect_key($arr, $this->schema);
}
- return array_intersect_key($arr, $this->schema);
- }
- /**
- * Whether the given argument is valid.
- *
- * @param mixed $arr
- * An array to be validated.
- * @return bool
- * TRUE if the argument is valid; FALSE otherwise.
- */
- public function isValid(mixed $arr): bool {
- try {
- $this($arr);
- }
- catch (Exception $e) {
- return FALSE;
+ /**
+ * Whether the given argument is valid.
+ *
+ * @param mixed $arr
+ * An array to be validated.
+ * @return bool
+ * TRUE if the argument is valid; FALSE otherwise.
+ */
+ public function isValid(mixed $arr): bool
+ {
+ try {
+ $this($arr);
+ } catch (Exception $e) {
+ return false;
+ }
+ return true;
}
- return TRUE;
- }
-
- /**
- * Whether the validator is conditional.
- *
- * @return bool
- * TRUE if the validator may or not be applied, depending on context. FALSE
- * if the validator will be applied unconditionally.
- */
- public function isConditional(): bool {
- return (bool) $this->conditional;
- }
+ /**
+ * Whether the validator is conditional.
+ *
+ * @return bool
+ * TRUE if the validator may or not be applied, depending on context. FALSE
+ * if the validator will be applied unconditionally.
+ */
+ public function isConditional(): bool
+ {
+ return (bool) $this->conditional;
+ }
}
diff --git a/src/Command/App/From/SourceSite/Drupal7Extension.php b/src/Command/App/From/SourceSite/Drupal7Extension.php
index 4c323f648..fe9cae37a 100644
--- a/src/Command/App/From/SourceSite/Drupal7Extension.php
+++ b/src/Command/App/From/SourceSite/Drupal7Extension.php
@@ -1,105 +1,112 @@
type = $type;
- $this->name = $name;
- $this->enabled = $enabled;
- $this->humanName = !empty($human_name) ? $human_name : $name;
- $this->version = $version;
- }
-
- /**
- * Creates an extension object given a Drush extension object.
- *
- * @param object $extension
- * An extension object as returned by drush_get_extensions().
- * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension
- * A new extension.
- */
- public static function createFromStdClass(object $extension): Drupal7Extension {
- return new static(
- $extension->type,
- $extension->name,
- $extension->status,
- $extension->humanName ?? $extension->name,
- $extension->version ?? 'Unknown',
- );
- }
-
- public function getName(): string {
- return $this->name;
- }
-
- public function getHumanName(): string {
- return $this->humanName;
- }
-
- public function getVersion(): string {
- return $this->version;
- }
-
- public function isModule(): bool {
- return $this->type === 'module';
- }
-
- public function isTheme(): bool {
- return $this->type === 'theme';
- }
-
- public function isEnabled(): bool {
- return $this->enabled;
- }
-
+final class Drupal7Extension implements ExtensionInterface
+{
+ /**
+ * The type of the extension.
+ *
+ * @var string
+ * Either 'module' or 'theme'.
+ */
+ protected string $type;
+
+ /**
+ * The name of the extension.
+ */
+ protected string $name;
+
+ /**
+ * Whether the extension is enabled or not.
+ */
+ protected bool $enabled;
+
+ /**
+ * The human-readable name of the extension.
+ */
+ protected string $humanName;
+
+ /**
+ * The extension's version.
+ */
+ protected string $version;
+
+ /**
+ * Extension constructor.
+ *
+ * @param string $type
+ * The extension type. Either 'module' or 'theme'.
+ * @param string $name
+ * The extension name.
+ * @param bool $enabled
+ * Whether the extension is enabled or not.
+ * @param string $human_name
+ * The human-readable name of the extension.
+ * @param string $version
+ * The extension version.
+ */
+ protected function __construct(string $type, string $name, bool $enabled, string $human_name, string $version)
+ {
+ assert(in_array($type, ['module', 'theme']));
+ $this->type = $type;
+ $this->name = $name;
+ $this->enabled = $enabled;
+ $this->humanName = !empty($human_name) ? $human_name : $name;
+ $this->version = $version;
+ }
+
+ /**
+ * Creates an extension object given a Drush extension object.
+ *
+ * @param object $extension
+ * An extension object as returned by drush_get_extensions().
+ * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension
+ * A new extension.
+ */
+ public static function createFromStdClass(object $extension): Drupal7Extension
+ {
+ return new static(
+ $extension->type,
+ $extension->name,
+ $extension->status,
+ $extension->humanName ?? $extension->name,
+ $extension->version ?? 'Unknown',
+ );
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getHumanName(): string
+ {
+ return $this->humanName;
+ }
+
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ public function isModule(): bool
+ {
+ return $this->type === 'module';
+ }
+
+ public function isTheme(): bool
+ {
+ return $this->type === 'theme';
+ }
+
+ public function isEnabled(): bool
+ {
+ return $this->enabled;
+ }
}
diff --git a/src/Command/App/From/SourceSite/Drupal7SiteInspector.php b/src/Command/App/From/SourceSite/Drupal7SiteInspector.php
index 8e03cfe19..78c51b85e 100644
--- a/src/Command/App/From/SourceSite/Drupal7SiteInspector.php
+++ b/src/Command/App/From/SourceSite/Drupal7SiteInspector.php
@@ -1,6 +1,6 @@
root = $drupal_root;
- $this->uri = $uri;
- }
-
- /**
- * {@inheritDoc}
- *
- * Uses drush to get all known extensions on the context Drupal 7 site.
- */
- protected function readExtensions(): array {
- $this->bootstrap();
- // @phpstan-ignore-next-line
- $enabled = system_list('module_enabled');
- // Special case to remove 'standard' from the module's list.
- unset($enabled['standard']);
- $modules = array_values(array_map(function (string $name) use ($enabled) {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- return (object) [
- 'name' => $name,
- 'status' => TRUE,
- 'type' => 'module',
- 'humanName' => $enabled[$name]->info['name'],
- 'version' => $enabled[$name]->info['version'],
- ];
- // phpcs:enable
- }, array_keys($enabled)));
- return array_map([Drupal7Extension::class, 'createFromStdClass'], $modules);
- }
-
- public function getPublicFilePath(): string {
- $this->bootstrap();
- // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/stream_wrappers.inc#L919
- // @phpstan-ignore-next-line
- return variable_get('file_public_path', conf_path() . '/files');
- }
-
- public function getPrivateFilePath(): ?string {
- $this->bootstrap();
- // @phpstan-ignore-next-line
- return variable_get('file_private_path', NULL);
- }
-
- /**
- * Bootstraps the inspected Drupal site.
- */
- protected function bootstrap(): void {
- static $bootstrapped;
- if ($bootstrapped) {
- return;
+final class Drupal7SiteInspector extends SiteInspectorBase
+{
+ /**
+ * The path to Drupal site root.
+ */
+ protected string $root;
+
+ /**
+ * The host name to use in order to resolve the appropriate settings.php.
+ */
+ public string $uri;
+
+ /**
+ * Drupal7SiteInspector constructor.
+ *
+ * @param string $drupal_root
+ * The path to Drupal site root.
+ * @param string $uri
+ * (optional) The host name to use in order to resolve the appropriate
+ * settings.php directory. Defaults to 'default'.
+ */
+ public function __construct(string $drupal_root, string $uri = 'default')
+ {
+ $this->root = $drupal_root;
+ $this->uri = $uri;
}
- $previous_directory = getcwd();
- chdir($this->root);
- if (!defined('DRUPAL_ROOT')) {
- define('DRUPAL_ROOT', $this->root);
+
+ /**
+ * {@inheritDoc}
+ *
+ * Uses drush to get all known extensions on the context Drupal 7 site.
+ */
+ protected function readExtensions(): array
+ {
+ $this->bootstrap();
+ // @phpstan-ignore-next-line
+ $enabled = system_list('module_enabled');
+ // Special case to remove 'standard' from the module's list.
+ unset($enabled['standard']);
+ $modules = array_values(array_map(function (string $name) use ($enabled) {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ return (object) [
+ 'name' => $name,
+ 'status' => true,
+ 'type' => 'module',
+ 'humanName' => $enabled[$name]->info['name'],
+ 'version' => $enabled[$name]->info['version'],
+ ];
+ // phpcs:enable
+ }, array_keys($enabled)));
+ return array_map([Drupal7Extension::class, 'createFromStdClass'], $modules);
}
- // phpcs:disable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable
- $_SERVER['HTTP_HOST'] = $this->uri;
- $_SERVER['REQUEST_URI'] = $this->uri . '/';
- $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] . 'index.php';
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['REQUEST_METHOD'] = 'GET';
- $_SERVER['SERVER_SOFTWARE'] = NULL;
- $_SERVER['HTTP_USER_AGENT'] = 'console';
- $_SERVER['SCRIPT_FILENAME'] = DRUPAL_ROOT . '/index.php';
- // phpcs:enable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable
- require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
- // @phpstan-ignore-next-line
- drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
- chdir($previous_directory);
- $bootstrapped = TRUE;
- }
- /**
- * Validates the given Drupal 7 application root.
- *
- * @param string $path
- * The path to validate.
- * @return string
- * The received Drupal 7 path, if it is valid, without trailing slashes.
- */
- public static function validateDrupal7Root(string $path): string {
- $path = rtrim($path, '/');
- if (!file_exists($path)) {
- throw new ValidatorException(sprintf("The path '%s' does not exist. Please enter the absolute path to a Drupal 7 application root.", $path));
+ public function getPublicFilePath(): string
+ {
+ $this->bootstrap();
+ // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/stream_wrappers.inc#L919
+ // @phpstan-ignore-next-line
+ return variable_get('file_public_path', conf_path() . '/files');
}
- if (!file_exists("$path/index.php")) {
- throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a index.php file.", $path));
+
+ public function getPrivateFilePath(): ?string
+ {
+ $this->bootstrap();
+ // @phpstan-ignore-next-line
+ return variable_get('file_private_path', null);
}
- if (!file_exists("$path/sites/default/default.settings.php")) {
- throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a sites/default/default.settings.php.", $path));
+
+ /**
+ * Bootstraps the inspected Drupal site.
+ */
+ protected function bootstrap(): void
+ {
+ static $bootstrapped;
+ if ($bootstrapped) {
+ return;
+ }
+ $previous_directory = getcwd();
+ chdir($this->root);
+ if (!defined('DRUPAL_ROOT')) {
+ define('DRUPAL_ROOT', $this->root);
+ }
+ // phpcs:disable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable
+ $_SERVER['HTTP_HOST'] = $this->uri;
+ $_SERVER['REQUEST_URI'] = $this->uri . '/';
+ $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] . 'index.php';
+ $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['SERVER_SOFTWARE'] = null;
+ $_SERVER['HTTP_USER_AGENT'] = 'console';
+ $_SERVER['SCRIPT_FILENAME'] = DRUPAL_ROOT . '/index.php';
+ // phpcs:enable SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable
+ require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
+ // @phpstan-ignore-next-line
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
+ chdir($previous_directory);
+ $bootstrapped = true;
}
- return $path;
- }
- /**
- * Determines the best URI to use for bootstrapping the source site.
- *
- * @param \Symfony\Component\Console\Input\InputInterface $input
- * The input passed into this Symfony command.
- * @param string $drupal_root
- * The root of the source site.
- * @return string
- * A URI string corresponding to an installed site.
- */
- public static function getSiteUri(InputInterface $input, string $drupal_root): string {
- // Construct a list of site directories which contain a settings.php file.
- $site_dirs = array_map(function ($path) use ($drupal_root) {
- return substr($path, strlen("$drupal_root/sites/"), -1 * strlen('/settings.php'));
- }, glob("$drupal_root/sites/*/settings.php"));
- // If the --drupal7-uri flag is defined, defer to it and attempt to ensure that it's
- // valid.
- if ($input->getOption('drupal7-uri') !== NULL) {
- $uri = $input->getOption('drupal7-uri');
- $sites_location = "$drupal_root/sites/sites.php";
- // If there isn't a sites.php file and the URI does not correspond to a
- // site directory, the site will be unable to bootstrap.
- if (!file_exists($sites_location) && !in_array($uri, $site_dirs, TRUE)) {
- throw new \InvalidArgumentException(
- sprintf('The given --drupal7-uri value does not correspond to an installed sites directory and a sites.php file could not be located.'),
- NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST
- );
- }
- // Parse the contents of sites.php.
- $sites = [];
- // This will override $sites.
- // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/bootstrap.inc#L563
- include $sites_location;
+ /**
+ * Validates the given Drupal 7 application root.
+ *
+ * @param string $path
+ * The path to validate.
+ * @return string
+ * The received Drupal 7 path, if it is valid, without trailing slashes.
+ */
+ public static function validateDrupal7Root(string $path): string
+ {
+ $path = rtrim($path, '/');
+ if (!file_exists($path)) {
+ throw new ValidatorException(sprintf("The path '%s' does not exist. Please enter the absolute path to a Drupal 7 application root.", $path));
+ }
+ if (!file_exists("$path/index.php")) {
+ throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a index.php file.", $path));
+ }
+ if (!file_exists("$path/sites/default/default.settings.php")) {
+ throw new ValidatorException(sprintf("The '%s' directory does not seem to be the root of a Drupal 7 application. It does not contain a sites/default/default.settings.php.", $path));
+ }
+ return $path;
+ }
- // @phpstan-ignore-next-line
- if (!empty($sites)) {
- // If the URI corresponds to a configuration in sites.php, then ensure
- // that the identified directory also has a settings.php file. If it
- // does not, then the site is probably not installed.
- if (isset($sites[$uri])) {
- if (!in_array($sites[$uri], $site_dirs, TRUE)) {
- throw new \InvalidArgumentException(
- sprintf('The given --drupal7-uri value corresponds to a site directory in sites.php, but that directory does not have a settings.php file. This typically means that the site has not been installed.'),
- NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST
- );
- }
- // The URI is assumed to be valid.
- return $uri;
+ /**
+ * Determines the best URI to use for bootstrapping the source site.
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * The input passed into this Symfony command.
+ * @param string $drupal_root
+ * The root of the source site.
+ * @return string
+ * A URI string corresponding to an installed site.
+ */
+ public static function getSiteUri(InputInterface $input, string $drupal_root): string
+ {
+ // Construct a list of site directories which contain a settings.php file.
+ $site_dirs = array_map(function ($path) use ($drupal_root) {
+ return substr($path, strlen("$drupal_root/sites/"), -1 * strlen('/settings.php'));
+ }, glob("$drupal_root/sites/*/settings.php"));
+ // If the --drupal7-uri flag is defined, defer to it and attempt to ensure that it's
+ // valid.
+ if ($input->getOption('drupal7-uri') !== null) {
+ $uri = $input->getOption('drupal7-uri');
+ $sites_location = "$drupal_root/sites/sites.php";
+ // If there isn't a sites.php file and the URI does not correspond to a
+ // site directory, the site will be unable to bootstrap.
+ if (!file_exists($sites_location) && !in_array($uri, $site_dirs, true)) {
+ throw new \InvalidArgumentException(
+ sprintf('The given --drupal7-uri value does not correspond to an installed sites directory and a sites.php file could not be located.'),
+ NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST
+ );
+ }
+ // Parse the contents of sites.php.
+ $sites = [];
+ // This will override $sites.
+ // @see https://git.drupalcode.org/project/drupal/-/blob/7.x/includes/bootstrap.inc#L563
+ include $sites_location;
+
+ // @phpstan-ignore-next-line
+ if (!empty($sites)) {
+ // If the URI corresponds to a configuration in sites.php, then ensure
+ // that the identified directory also has a settings.php file. If it
+ // does not, then the site is probably not installed.
+ if (isset($sites[$uri])) {
+ if (!in_array($sites[$uri], $site_dirs, true)) {
+ throw new \InvalidArgumentException(
+ sprintf('The given --drupal7-uri value corresponds to a site directory in sites.php, but that directory does not have a settings.php file. This typically means that the site has not been installed.'),
+ NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST
+ );
+ }
+ // The URI is assumed to be valid.
+ return $uri;
+ }
+ // The given URI doesn't match anything in sites.php.
+ throw new \InvalidArgumentException(
+ sprintf('The given --drupal7-uri value does not correspond to any configuration in sites.php.'),
+ NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST
+ );
+ }
+
+ if (in_array($uri, $site_dirs, true)) {
+ return $uri;
+ }
}
- // The given URI doesn't match anything in sites.php.
+ // There was no --drupal7-uri flag specified, so attempt to determine a sane
+ // default. If there is only one possible site, use it. If there is more
+ // than one, but there is a default directory with a settings.php, use that.
+ if (count($site_dirs) === 1) {
+ return current($site_dirs);
+ } elseif (in_array('default', $site_dirs, true)) {
+ return 'default';
+ }
+ // A URI corresponding to a site directory could not be determined, rather
+ // than make a faulty assumption (e.g. use the first found), exit.
throw new \InvalidArgumentException(
- sprintf('The given --drupal7-uri value does not correspond to any configuration in sites.php.'),
- NewFromDrupal7Command::ERR_UNRECOGNIZED_HOST
+ sprintf('A Drupal 7 installation could not be located.'),
+ NewFromDrupal7Command::ERR_INDETERMINATE_SITE
);
- }
-
- if (in_array($uri, $site_dirs, TRUE)) {
- return $uri;
- }
- }
- // There was no --drupal7-uri flag specified, so attempt to determine a sane
- // default. If there is only one possible site, use it. If there is more
- // than one, but there is a default directory with a settings.php, use that.
- if (count($site_dirs) === 1) {
- return current($site_dirs);
}
- elseif (in_array('default', $site_dirs, TRUE)) {
- return 'default';
- }
- // A URI corresponding to a site directory could not be determined, rather
- // than make a faulty assumption (e.g. use the first found), exit.
- throw new \InvalidArgumentException(
- sprintf('A Drupal 7 installation could not be located.'),
- NewFromDrupal7Command::ERR_INDETERMINATE_SITE
- );
- }
-
}
diff --git a/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php b/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php
index 4fcff9ea7..a382738ae 100644
--- a/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php
+++ b/src/Command/App/From/SourceSite/ExportedDrupal7ExtensionsInspector.php
@@ -1,66 +1,71 @@
extensions;
+ }
- /**
- * {@inheritDoc}
- */
- protected function readExtensions(): array {
- return $this->extensions;
- }
+ public function getPublicFilePath(): string
+ {
+ return 'sites/default/files';
+ }
- public function getPublicFilePath(): string {
- return 'sites/default/files';
- }
-
- public function getPrivateFilePath(): ?string {
- return NULL;
- }
-
- /**
- * Reads an extensions resource into extensions objects.
- *
- * @param resource $extensions_resource
- * A serialized extensions resource from which to parse extensions.
- * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension[]
- * An array of extensions.
- */
- protected static function parseExtensionsFromResource($extensions_resource): array {
- return array_map(function (array $extension) {
- $extension['status'] = $extension['enabled'];
- return Drupal7Extension::createFromStdClass((object) $extension);
- }, static::parseJsonResource($extensions_resource));
- }
+ public function getPrivateFilePath(): ?string
+ {
+ return null;
+ }
+ /**
+ * Reads an extensions resource into extensions objects.
+ *
+ * @param resource $extensions_resource
+ * A serialized extensions resource from which to parse extensions.
+ * @return \Acquia\Cli\Command\App\From\SourceSite\Drupal7Extension[]
+ * An array of extensions.
+ */
+ protected static function parseExtensionsFromResource($extensions_resource): array
+ {
+ return array_map(function (array $extension) {
+ $extension['status'] = $extension['enabled'];
+ return Drupal7Extension::createFromStdClass((object) $extension);
+ }, static::parseJsonResource($extensions_resource));
+ }
}
diff --git a/src/Command/App/From/SourceSite/ExtensionInterface.php b/src/Command/App/From/SourceSite/ExtensionInterface.php
index c7607fbb7..371ee573a 100644
--- a/src/Command/App/From/SourceSite/ExtensionInterface.php
+++ b/src/Command/App/From/SourceSite/ExtensionInterface.php
@@ -1,60 +1,59 @@
readExtensions(), function (ExtensionInterface $extension) use ($state_flags, $type_flags) {
- // Generate a flag for the extension's enabled/disabled state.
- $has = $extension->isEnabled() ? SiteInspectorInterface::FLAG_EXTENSION_ENABLED : SiteInspectorInterface::FLAG_EXTENSION_DISABLED;
- // Incorporate the extension's type.
- $has = $has | ($extension->isModule() ? SiteInspectorInterface::FLAG_EXTENSION_MODULE : 0);
- $has = $has | ($extension->isTheme() ? SiteInspectorInterface::FLAG_EXTENSION_THEME : 0);
- // TRUE if the extension has a flag in $type_flags AND a flag in
- // $state_flags, FALSE otherwise.
- return ($has & $type_flags) && ($has & $state_flags);
- });
- }
-
- /**
- * Returns a list of extensions discovered on the inspected site.
- *
- * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
- * An array of extensions discovered on the inspected source site.
- */
- abstract protected function readExtensions(): array;
+abstract class SiteInspectorBase implements SiteInspectorInterface
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function getExtensions(int $flags): array
+ {
+ $state_flags = $flags & (SiteInspectorInterface::FLAG_EXTENSION_ENABLED | SiteInspectorInterface::FLAG_EXTENSION_DISABLED);
+ $type_flags = $flags & (SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_THEME);
+ return array_filter($this->readExtensions(), function (ExtensionInterface $extension) use ($state_flags, $type_flags) {
+ // Generate a flag for the extension's enabled/disabled state.
+ $has = $extension->isEnabled() ? SiteInspectorInterface::FLAG_EXTENSION_ENABLED : SiteInspectorInterface::FLAG_EXTENSION_DISABLED;
+ // Incorporate the extension's type.
+ $has = $has | ($extension->isModule() ? SiteInspectorInterface::FLAG_EXTENSION_MODULE : 0);
+ $has = $has | ($extension->isTheme() ? SiteInspectorInterface::FLAG_EXTENSION_THEME : 0);
+ // TRUE if the extension has a flag in $type_flags AND a flag in
+ // $state_flags, FALSE otherwise.
+ return ($has & $type_flags) && ($has & $state_flags);
+ });
+ }
+ /**
+ * Returns a list of extensions discovered on the inspected site.
+ *
+ * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
+ * An array of extensions discovered on the inspected source site.
+ */
+ abstract protected function readExtensions(): array;
}
diff --git a/src/Command/App/From/SourceSite/SiteInspectorInterface.php b/src/Command/App/From/SourceSite/SiteInspectorInterface.php
index 82101bac8..98181799c 100644
--- a/src/Command/App/From/SourceSite/SiteInspectorInterface.php
+++ b/src/Command/App/From/SourceSite/SiteInspectorInterface.php
@@ -1,69 +1,69 @@
getExtensions(Drupal7SiteInspector::FLAG_EXTENSION_ENABLED|Drupal7SiteInspector::FLAG_EXTENSION_MODULE);
- * @endcode
- * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
- * An array of identified extensions, filtered to contain only those
- * included by the given flags.
- */
- public function getExtensions(int $flags): array;
+ /**
+ * Gets extensions on an inspected site.
+ *
+ * @param int $flags
+ * Bitwise flags indicting various subsets of extensions to be returned.
+ * Omitting flags omits those extensions from the return value. I.e. if the
+ * FLAG_EXTENSION_ENABLED flag is given, but not the FLAG_EXTENSION_DISABLED
+ * flag, then only enabled extensions will be returned. In the example
+ * below, all enabled modules will be returned. Themes and disabled modules
+ * will be excluded.
+ * @code
+ * $inspector->getExtensions(Drupal7SiteInspector::FLAG_EXTENSION_ENABLED|Drupal7SiteInspector::FLAG_EXTENSION_MODULE);
+ * @endcode
+ * @return \Acquia\Cli\Command\App\From\SourceSite\ExtensionInterface[]
+ * An array of identified extensions, filtered to contain only those
+ * included by the given flags.
+ */
+ public function getExtensions(int $flags): array;
- /**
- * Gets the public file path relative to the Drupal root.
- */
- public function getPublicFilePath(): string;
-
- /**
- * Gets the private file path relative to the Drupal root, if it exists.
- *
- * @return string|null
- * NULL if the the inspected site does not use private files, a string
- * otherwise.
- */
- public function getPrivateFilePath(): ?string;
+ /**
+ * Gets the public file path relative to the Drupal root.
+ */
+ public function getPublicFilePath(): string;
+ /**
+ * Gets the private file path relative to the Drupal root, if it exists.
+ *
+ * @return string|null
+ * NULL if the the inspected site does not use private files, a string
+ * otherwise.
+ */
+ public function getPrivateFilePath(): ?string;
}
diff --git a/src/Command/App/LinkCommand.php b/src/Command/App/LinkCommand.php
index d0c626a36..4247a0dc9 100644
--- a/src/Command/App/LinkCommand.php
+++ b/src/Command/App/LinkCommand.php
@@ -1,6 +1,6 @@
acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->validateCwdIsValidDrupalProject();
- if ($cloudApplicationUuid = $this->getCloudUuidFromDatastore()) {
- $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
- $output->writeln('This repository is already linked to Cloud application ' . $cloudApplication->name . '>. Run acli unlink> to unlink it.');
- return 1;
+final class LinkCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this->acceptApplicationUuid();
}
- $this->determineCloudApplication(TRUE);
-
- return Command::SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->validateCwdIsValidDrupalProject();
+ if ($cloudApplicationUuid = $this->getCloudUuidFromDatastore()) {
+ $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
+ $output->writeln('This repository is already linked to Cloud application ' . $cloudApplication->name . '>. Run acli unlink> to unlink it.');
+ return 1;
+ }
+ $this->determineCloudApplication(true);
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/App/LogTailCommand.php b/src/Command/App/LogTailCommand.php
index 543d70040..5fd9cf648 100644
--- a/src/Command/App/LogTailCommand.php
+++ b/src/Command/App/LogTailCommand.php
@@ -1,6 +1,6 @@
localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger);
+ }
- public function __construct(
- public LocalMachineHelper $localMachineHelper,
- protected CloudDataStore $datastoreCloud,
- protected AcquiaCliDatastore $datastoreAcli,
- protected ApiCredentialsInterface $cloudCredentials,
- protected TelemetryHelper $telemetryHelper,
- protected string $projectDir,
- protected ClientService $cloudApiClientService,
- public SshHelper $sshHelper,
- protected string $sshDir,
- LoggerInterface $logger,
- protected LogstreamManager $logstreamManager,
- ) {
- parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger);
- }
-
- protected function configure(): void {
- $this
- ->acceptEnvironmentId();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $environment = $this->determineEnvironment($input, $output);
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $logs = $this->promptChooseLogs();
- $logTypes = array_map(static function (mixed $log) {
- return $log['type'];
- }, $logs);
- $logsResource = new Logs($acquiaCloudClient);
- $stream = $logsResource->stream($environment->uuid);
- $this->logstreamManager->setParams($stream->logstream->params);
- $this->logstreamManager->setColourise(TRUE);
- $this->logstreamManager->setLogTypeFilter($logTypes);
- $output->writeln('Streaming has started and new logs will appear below. Use Ctrl+C to exit.');
- $this->logstreamManager->stream();
- return Command::SUCCESS;
- }
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId();
+ }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $environment = $this->determineEnvironment($input, $output);
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $logs = $this->promptChooseLogs();
+ $logTypes = array_map(static function (mixed $log) {
+ return $log['type'];
+ }, $logs);
+ $logsResource = new Logs($acquiaCloudClient);
+ $stream = $logsResource->stream($environment->uuid);
+ $this->logstreamManager->setParams($stream->logstream->params);
+ $this->logstreamManager->setColourise(true);
+ $this->logstreamManager->setLogTypeFilter($logTypes);
+ $output->writeln('Streaming has started and new logs will appear below. Use Ctrl+C to exit.');
+ $this->logstreamManager->stream();
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/App/NewCommand.php b/src/Command/App/NewCommand.php
index 99a576c15..df2efb75b 100644
--- a/src/Command/App/NewCommand.php
+++ b/src/Command/App/NewCommand.php
@@ -1,6 +1,6 @@
addArgument('directory', InputArgument::OPTIONAL, 'The destination directory');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->output->writeln('Acquia recommends most customers use acquia/drupal-recommended-project> to setup a Drupal project, which includes useful utilities such as Acquia Connector.');
- $this->output->writeln('acquia/next-acms> is a starter template for building a headless site powered by Acquia CMS and Next.js.');
- $distros = [
- 'acquia_drupal_recommended' => 'acquia/drupal-recommended-project',
- 'acquia_next_acms' => 'acquia/next-acms',
- ];
- $project = $this->io->choice('Choose a starting project', array_values($distros), $distros['acquia_drupal_recommended']);
- $project = array_search($project, $distros, TRUE);
-
- if ($input->hasArgument('directory') && $input->getArgument('directory')) {
- $dir = Path::canonicalize($input->getArgument('directory'));
- $dir = Path::makeAbsolute($dir, getcwd());
+final class NewCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('directory', InputArgument::OPTIONAL, 'The destination directory');
}
- else if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
- $dir = '/home/ide/project';
- }
- else {
- $dir = Path::makeAbsolute($project, getcwd());
- }
-
- $output->writeln('Creating project. This may take a few minutes.');
- if ($project === 'acquia_next_acms') {
- $successMessage = "New Next JS project created in $dir. 🎉";
- $this->localMachineHelper->checkRequiredBinariesExist(['node']);
- $this->createNextJsProject($dir);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->output->writeln('Acquia recommends most customers use acquia/drupal-recommended-project> to setup a Drupal project, which includes useful utilities such as Acquia Connector.');
+ $this->output->writeln('acquia/next-acms> is a starter template for building a headless site powered by Acquia CMS and Next.js.');
+ $distros = [
+ 'acquia_drupal_recommended' => 'acquia/drupal-recommended-project',
+ 'acquia_next_acms' => 'acquia/next-acms',
+ ];
+ $project = $this->io->choice('Choose a starting project', array_values($distros), $distros['acquia_drupal_recommended']);
+ $project = array_search($project, $distros, true);
+
+ if ($input->hasArgument('directory') && $input->getArgument('directory')) {
+ $dir = Path::canonicalize($input->getArgument('directory'));
+ $dir = Path::makeAbsolute($dir, getcwd());
+ } elseif (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
+ $dir = '/home/ide/project';
+ } else {
+ $dir = Path::makeAbsolute($project, getcwd());
+ }
+
+ $output->writeln('Creating project. This may take a few minutes.');
+
+ if ($project === 'acquia_next_acms') {
+ $successMessage = "New Next JS project created in $dir. 🎉";
+ $this->localMachineHelper->checkRequiredBinariesExist(['node']);
+ $this->createNextJsProject($dir);
+ } else {
+ $successMessage = "New 💧 Drupal project created in $dir. 🎉";
+ $this->localMachineHelper->checkRequiredBinariesExist(['composer']);
+ $this->createDrupalProject($distros[$project], $dir);
+ }
+
+ $this->initializeGitRepository($dir);
+
+ $output->writeln('');
+ $output->writeln($successMessage);
+
+ return Command::SUCCESS;
}
- else {
- $successMessage = "New 💧 Drupal project created in $dir. 🎉";
- $this->localMachineHelper->checkRequiredBinariesExist(['composer']);
- $this->createDrupalProject($distros[$project], $dir);
- }
-
- $this->initializeGitRepository($dir);
- $output->writeln('');
- $output->writeln($successMessage);
-
- return Command::SUCCESS;
- }
-
- private function createNextJsProject(string $dir): void {
- $process = $this->localMachineHelper->execute([
- 'npx',
- 'create-next-app',
- '-e',
- 'https://github.com/acquia/next-acms/tree/main/starters/basic-starter',
- $dir,
- ]);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Unable to create new next-acms project.");
+ private function createNextJsProject(string $dir): void
+ {
+ $process = $this->localMachineHelper->execute([
+ 'npx',
+ 'create-next-app',
+ '-e',
+ 'https://github.com/acquia/next-acms/tree/main/starters/basic-starter',
+ $dir,
+ ]);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Unable to create new next-acms project.");
+ }
}
- }
-
- private function createDrupalProject(string $project, string $dir): void {
- $process = $this->localMachineHelper->execute([
- 'composer',
- 'create-project',
- $project,
- $dir,
- '--no-interaction',
- ]);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Unable to create new project.");
- }
- }
- private function initializeGitRepository(string $dir): void {
- if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) {
- $this->logger->debug('.git directory detected, skipping Git repo initialization');
- return;
+ private function createDrupalProject(string $project, string $dir): void
+ {
+ $process = $this->localMachineHelper->execute([
+ 'composer',
+ 'create-project',
+ $project,
+ $dir,
+ '--no-interaction',
+ ]);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Unable to create new project.");
+ }
}
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $this->localMachineHelper->execute([
- 'git',
- 'init',
- '--initial-branch=main',
- ], NULL, $dir);
-
- $this->localMachineHelper->execute([
- 'git',
- 'add',
- '-A',
- ], NULL, $dir);
-
- $this->localMachineHelper->execute([
- 'git',
- 'commit',
- '--message',
- 'Initial commit.',
- '--quiet',
- ], NULL, $dir);
- // @todo Check that this was successful!
- }
+ private function initializeGitRepository(string $dir): void
+ {
+ if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) {
+ $this->logger->debug('.git directory detected, skipping Git repo initialization');
+ return;
+ }
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $this->localMachineHelper->execute([
+ 'git',
+ 'init',
+ '--initial-branch=main',
+ ], null, $dir);
+
+ $this->localMachineHelper->execute([
+ 'git',
+ 'add',
+ '-A',
+ ], null, $dir);
+
+ $this->localMachineHelper->execute([
+ 'git',
+ 'commit',
+ '--message',
+ 'Initial commit.',
+ '--quiet',
+ ], null, $dir);
+ // @todo Check that this was successful!
+ }
}
diff --git a/src/Command/App/NewFromDrupal7Command.php b/src/Command/App/NewFromDrupal7Command.php
index 75c67ded4..cf1dba8e3 100644
--- a/src/Command/App/NewFromDrupal7Command.php
+++ b/src/Command/App/NewFromDrupal7Command.php
@@ -1,6 +1,6 @@
addOption('drupal7-directory', 'source', InputOption::VALUE_OPTIONAL, 'The root of the Drupal 7 application')
- ->addOption('drupal7-uri', 'uri', InputOption::VALUE_OPTIONAL, 'Only necessary in case of a multisite. If a single site, this will be computed automatically.')
- ->addOption('stored-analysis', 'analysis', InputOption::VALUE_OPTIONAL, 'As an alternative to drupal7-directory, it is possible to pass a stored analysis.')
- ->addOption('recommendations', 'recommendations', InputOption::VALUE_OPTIONAL, 'Overrides the default recommendations.')
- ->addOption('directory', 'destination', InputOption::VALUE_OPTIONAL, 'The directory where to generate the new application.');
- }
-
- private function getInspector(InputInterface $input): SiteInspectorInterface {
- if ($input->getOption('stored-analysis') !== NULL) {
- $analysis_json = $input->getOption('stored-analysis');
- $extensions_resource = fopen($analysis_json, 'r');
- $inspector = ExportedDrupal7ExtensionsInspector::createFromResource($extensions_resource);
- fclose($extensions_resource);
- return $inspector;
+final class NewFromDrupal7Command extends CommandBase
+{
+ /**
+ * Exit code raised when the URI flag does not correspond to configuration.
+ *
+ * This typically indicates the value of the --drupal7-uri flag does not
+ * correspond to any configuration in a Drupal site's sites/sites.php file.
+ */
+ public const ERR_UNRECOGNIZED_HOST = 3;
+
+ /**
+ * Exit code raised when a Drupal 7 installation cannot be determined.
+ *
+ * This indicates the --drupal7-uri was not given and a sane default site could not be
+ * determined.
+ */
+ public const ERR_INDETERMINATE_SITE = 4;
+
+ protected function configure(): void
+ {
+ $this
+ ->addOption('drupal7-directory', 'source', InputOption::VALUE_OPTIONAL, 'The root of the Drupal 7 application')
+ ->addOption('drupal7-uri', 'uri', InputOption::VALUE_OPTIONAL, 'Only necessary in case of a multisite. If a single site, this will be computed automatically.')
+ ->addOption('stored-analysis', 'analysis', InputOption::VALUE_OPTIONAL, 'As an alternative to drupal7-directory, it is possible to pass a stored analysis.')
+ ->addOption('recommendations', 'recommendations', InputOption::VALUE_OPTIONAL, 'Overrides the default recommendations.')
+ ->addOption('directory', 'destination', InputOption::VALUE_OPTIONAL, 'The directory where to generate the new application.');
}
- // First: Determine the Drupal 7 root.
- $d7_root = $this->determineOption('drupal7-directory', FALSE, Drupal7SiteInspector::validateDrupal7Root(...), NULL, '.');
-
- // Second, determine which "sites" subdirectory is being assessed.
- $uri = Drupal7SiteInspector::getSiteUri($input, $d7_root);
-
- return new Drupal7SiteInspector($d7_root, $uri);
- }
-
- private function getLocation(string $location, bool $should_exist = TRUE): string {
- if (strpos($location, '://') === FALSE) {
- $file_exists = file_exists($location);
- if ($file_exists && !$should_exist) {
- throw new ValidatorException(sprintf('The %s directory already exists.', $location));
- }
- elseif (!$file_exists && $should_exist) {
- throw new ValidatorException(sprintf('%s could not be located. Check that the path is correct and try again.', $location));
- }
- if (strpos($location, '.') === 0 || !static::isAbsolutePath($location)) {
- $absolute = getcwd() . '/' . $location;
- $location = $should_exist ? realpath($absolute) : $absolute;
- }
- }
- return $location;
- }
+ private function getInspector(InputInterface $input): SiteInspectorInterface
+ {
+ if ($input->getOption('stored-analysis') !== null) {
+ $analysis_json = $input->getOption('stored-analysis');
+ $extensions_resource = fopen($analysis_json, 'r');
+ $inspector = ExportedDrupal7ExtensionsInspector::createFromResource($extensions_resource);
+ fclose($extensions_resource);
+ return $inspector;
+ }
- private static function isAbsolutePath(string $path): bool {
- // @see https://stackoverflow.com/a/23570509
- return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0;
- }
+ // First: Determine the Drupal 7 root.
+ $d7_root = $this->determineOption('drupal7-directory', false, Drupal7SiteInspector::validateDrupal7Root(...), null, '.');
- protected function execute(InputInterface $input, OutputInterface $output): int {
- try {
- $inspector = $this->getInspector($input);
- }
- catch (\Exception $e) {
- $this->io->error($e->getMessage());
- // Important: ensure that the unique error code that ::getSiteUri()
- // computed is passed on, to enable scripting this command.
- return $e->getCode();
- }
+ // Second, determine which "sites" subdirectory is being assessed.
+ $uri = Drupal7SiteInspector::getSiteUri($input, $d7_root);
- // Now the Drupal 7 site can be inspected. Inform the user.
- $output->writeln('🤖 Scanning Drupal 7 site.');
- $extensions = $inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED);
- $module_count = count($extensions);
- $system_module_version = array_reduce(
- array_filter($extensions, fn (ExtensionInterface $extension) => $extension->isModule() && $extension->getName() === 'system'),
- fn (mixed $carry, ExtensionInterface $extension) => $extension->getVersion()
- );
- $site_location = property_exists($inspector, 'uri') ? 'sites/' . $inspector->uri : '';
- $output->writeln(sprintf("👍 Found Drupal 7 site (%s to be precise) at %s, with %d modules enabled!", $system_module_version, $site_location, $module_count));
-
- // Parse config for project builder.
- $configuration_location = __DIR__ . '/../../../config/from_d7_config.json';
- $config_resource = fopen($configuration_location, 'r');
- $configuration = Configuration::createFromResource($config_resource);
- fclose($config_resource);
-
- // Parse recommendations for project builder.
- $recommendations_location = "https://git.drupalcode.org/project/acquia_migrate/-/raw/recommendations/recommendations.json";
- if ($input->getOption('recommendations') !== NULL) {
- $raw_recommendations_location = $input->getOption('recommendations');
- try {
- $recommendations_location = $this->getLocation($raw_recommendations_location);
- }
- catch (\InvalidArgumentException $e) {
- $this->io->error($e->getMessage());
- return Command::FAILURE;
- }
- }
- // PHP defaults to no user agent. (Drupal.org's) GitLab requires it.
- // @see https://www.php.net/manual/en/filesystem.configuration.php#ini.user-agent
- ini_set('user_agent', 'ACLI');
- $recommendations_resource = fopen($recommendations_location, 'r');
- $recommendations = Recommendations::createFromResource($recommendations_resource);
- fclose($recommendations_resource);
-
- // Build project (in memory) using the configuration and the given
- // recommendations from the inspected Drupal 7 site and inform the user.
- $output->writeln('🤖 Computing recommendations for this Drupal 7 site…');
- $project_builder = new ProjectBuilder($configuration, new Resolver($inspector, $recommendations), $inspector);
- $results = $project_builder->buildProject();
- $unique_patch_count = array_reduce(
- $results['rootPackageDefinition']['extra']['patches'],
- fn (array $unique_patches, array $patches) => array_unique(array_merge($unique_patches, array_values($patches))),
- []
- );
- $output->writeln(sprintf(
- "🥳 Great news: found %d recommendations that apply to this Drupal 7 site, resulting in a composer.json with:\n\t- %d packages\n\t- %d patches\n\t- %d modules to be installed!",
- count($results['recommendations']),
- count($results['rootPackageDefinition']['require']),
- $unique_patch_count,
- count($results['installModules']),
- ));
-
- // Ask where to store the generated project (in other words: where to write
- // a composer.json file). If a directory path is passed, assume the user
- // knows what they're doing.
- if ($input->getOption('directory') === NULL) {
- $answer = $this->io->ask(
- 'Where should the generated composer.json be written?',
- NULL,
- function (mixed $path): string {
- if (!is_string($path) || !file_exists($path) || file_exists("$path/composer.json")) {
- throw new ValidatorException(sprintf("The '%s' directory either does not exist or it already contains a composer.json file.", $path));
- }
- return $path;
- },
- );
- $input->setOption('directory', $answer);
- }
- $dir = $input->getOption('directory');
-
- // Create the info metadata array, including a complete root. Write this to
- // a metadata JSON file in the given directory. Also generate a
- // composer.json from this. Initialize a new Git repo and commit both.
- $data = array_merge(
- ['generated' => date(DATE_ATOM)],
- $project_builder->buildProject()
- );
- $json_encode_flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
- file_put_contents("$dir/acli-generated-project-metadata.json", json_encode($data, $json_encode_flags));
- file_put_contents("$dir/composer.json", json_encode($data['rootPackageDefinition'], $json_encode_flags));
- $this->initializeGitRepository($dir);
- $output->writeln('🚀 Generated composer.json and committed to a new git repo.');
- $output->writeln('');
-
- // Helpfully automatically run `composer install`, but equally helpfully do
- // not commit it yet, to allow the user to choose whether to commit build
- // artifacts.
- $output->writeln('⏳ Installing. This may take a few minutes.');
- $this->localMachineHelper->checkRequiredBinariesExist(['composer']);
- $process = $this->localMachineHelper->execute([
- 'composer',
- 'install',
- '--working-dir',
- $dir,
- '--no-interaction',
- ]);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Unable to create new project.");
+ return new Drupal7SiteInspector($d7_root, $uri);
}
- $output->writeln('');
- $output->writeln("New 💧 Drupal project created in $dir. 🎉");
+ private function getLocation(string $location, bool $should_exist = true): string
+ {
+ if (strpos($location, '://') === false) {
+ $file_exists = file_exists($location);
+ if ($file_exists && !$should_exist) {
+ throw new ValidatorException(sprintf('The %s directory already exists.', $location));
+ } elseif (!$file_exists && $should_exist) {
+ throw new ValidatorException(sprintf('%s could not be located. Check that the path is correct and try again.', $location));
+ }
+ if (strpos($location, '.') === 0 || !static::isAbsolutePath($location)) {
+ $absolute = getcwd() . '/' . $location;
+ $location = $should_exist ? realpath($absolute) : $absolute;
+ }
+ }
+ return $location;
+ }
- return Command::SUCCESS;
- }
+ private static function isAbsolutePath(string $path): bool
+ {
+ // @see https://stackoverflow.com/a/23570509
+ return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0;
+ }
- private function initializeGitRepository(string $dir): void {
- if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) {
- $this->logger->debug('.git directory detected, skipping Git repo initialization');
- return;
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $inspector = $this->getInspector($input);
+ } catch (\Exception $e) {
+ $this->io->error($e->getMessage());
+ // Important: ensure that the unique error code that ::getSiteUri()
+ // computed is passed on, to enable scripting this command.
+ return $e->getCode();
+ }
+
+ // Now the Drupal 7 site can be inspected. Inform the user.
+ $output->writeln('🤖 Scanning Drupal 7 site.');
+ $extensions = $inspector->getExtensions(SiteInspectorInterface::FLAG_EXTENSION_MODULE | SiteInspectorInterface::FLAG_EXTENSION_ENABLED);
+ $module_count = count($extensions);
+ $system_module_version = array_reduce(
+ array_filter($extensions, fn (ExtensionInterface $extension) => $extension->isModule() && $extension->getName() === 'system'),
+ fn (mixed $carry, ExtensionInterface $extension) => $extension->getVersion()
+ );
+ $site_location = property_exists($inspector, 'uri') ? 'sites/' . $inspector->uri : '';
+ $output->writeln(sprintf("👍 Found Drupal 7 site (%s to be precise) at %s, with %d modules enabled!", $system_module_version, $site_location, $module_count));
+
+ // Parse config for project builder.
+ $configuration_location = __DIR__ . '/../../../config/from_d7_config.json';
+ $config_resource = fopen($configuration_location, 'r');
+ $configuration = Configuration::createFromResource($config_resource);
+ fclose($config_resource);
+
+ // Parse recommendations for project builder.
+ $recommendations_location = "https://git.drupalcode.org/project/acquia_migrate/-/raw/recommendations/recommendations.json";
+ if ($input->getOption('recommendations') !== null) {
+ $raw_recommendations_location = $input->getOption('recommendations');
+ try {
+ $recommendations_location = $this->getLocation($raw_recommendations_location);
+ } catch (\InvalidArgumentException $e) {
+ $this->io->error($e->getMessage());
+ return Command::FAILURE;
+ }
+ }
+ // PHP defaults to no user agent. (Drupal.org's) GitLab requires it.
+ // @see https://www.php.net/manual/en/filesystem.configuration.php#ini.user-agent
+ ini_set('user_agent', 'ACLI');
+ $recommendations_resource = fopen($recommendations_location, 'r');
+ $recommendations = Recommendations::createFromResource($recommendations_resource);
+ fclose($recommendations_resource);
+
+ // Build project (in memory) using the configuration and the given
+ // recommendations from the inspected Drupal 7 site and inform the user.
+ $output->writeln('🤖 Computing recommendations for this Drupal 7 site…');
+ $project_builder = new ProjectBuilder($configuration, new Resolver($inspector, $recommendations), $inspector);
+ $results = $project_builder->buildProject();
+ $unique_patch_count = array_reduce(
+ $results['rootPackageDefinition']['extra']['patches'],
+ fn (array $unique_patches, array $patches) => array_unique(array_merge($unique_patches, array_values($patches))),
+ []
+ );
+ $output->writeln(sprintf(
+ "🥳 Great news: found %d recommendations that apply to this Drupal 7 site, resulting in a composer.json with:\n\t- %d packages\n\t- %d patches\n\t- %d modules to be installed!",
+ count($results['recommendations']),
+ count($results['rootPackageDefinition']['require']),
+ $unique_patch_count,
+ count($results['installModules']),
+ ));
+
+ // Ask where to store the generated project (in other words: where to write
+ // a composer.json file). If a directory path is passed, assume the user
+ // knows what they're doing.
+ if ($input->getOption('directory') === null) {
+ $answer = $this->io->ask(
+ 'Where should the generated composer.json be written?',
+ null,
+ function (mixed $path): string {
+ if (!is_string($path) || !file_exists($path) || file_exists("$path/composer.json")) {
+ throw new ValidatorException(sprintf("The '%s' directory either does not exist or it already contains a composer.json file.", $path));
+ }
+ return $path;
+ },
+ );
+ $input->setOption('directory', $answer);
+ }
+ $dir = $input->getOption('directory');
+
+ // Create the info metadata array, including a complete root. Write this to
+ // a metadata JSON file in the given directory. Also generate a
+ // composer.json from this. Initialize a new Git repo and commit both.
+ $data = array_merge(
+ ['generated' => date(DATE_ATOM)],
+ $project_builder->buildProject()
+ );
+ $json_encode_flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
+ file_put_contents("$dir/acli-generated-project-metadata.json", json_encode($data, $json_encode_flags));
+ file_put_contents("$dir/composer.json", json_encode($data['rootPackageDefinition'], $json_encode_flags));
+ $this->initializeGitRepository($dir);
+ $output->writeln('🚀 Generated composer.json and committed to a new git repo.');
+ $output->writeln('');
+
+ // Helpfully automatically run `composer install`, but equally helpfully do
+ // not commit it yet, to allow the user to choose whether to commit build
+ // artifacts.
+ $output->writeln('⏳ Installing. This may take a few minutes.');
+ $this->localMachineHelper->checkRequiredBinariesExist(['composer']);
+ $process = $this->localMachineHelper->execute([
+ 'composer',
+ 'install',
+ '--working-dir',
+ $dir,
+ '--no-interaction',
+ ]);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Unable to create new project.");
+ }
+
+ $output->writeln('');
+ $output->writeln("New 💧 Drupal project created in $dir. 🎉");
+
+ return Command::SUCCESS;
}
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $this->localMachineHelper->execute([
- 'git',
- 'init',
- '--initial-branch=main',
- '--quiet',
- ], NULL, $dir);
-
- $this->localMachineHelper->execute([
- 'git',
- 'add',
- '-A',
- ], NULL, $dir);
-
- $this->localMachineHelper->execute([
- 'git',
- 'commit',
- '--message',
- "Generated by Acquia CLI's app:new:from:drupal7.",
- '--quiet',
- ], NULL, $dir);
- // @todo Check that this was successful!
- }
+ private function initializeGitRepository(string $dir): void
+ {
+ if ($this->localMachineHelper->getFilesystem()->exists(Path::join($dir, '.git'))) {
+ $this->logger->debug('.git directory detected, skipping Git repo initialization');
+ return;
+ }
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $this->localMachineHelper->execute([
+ 'git',
+ 'init',
+ '--initial-branch=main',
+ '--quiet',
+ ], null, $dir);
+
+ $this->localMachineHelper->execute([
+ 'git',
+ 'add',
+ '-A',
+ ], null, $dir);
+
+ $this->localMachineHelper->execute([
+ 'git',
+ 'commit',
+ '--message',
+ "Generated by Acquia CLI's app:new:from:drupal7.",
+ '--quiet',
+ ], null, $dir);
+ // @todo Check that this was successful!
+ }
}
diff --git a/src/Command/App/TaskWaitCommand.php b/src/Command/App/TaskWaitCommand.php
index 501afb8c5..4c0585720 100644
--- a/src/Command/App/TaskWaitCommand.php
+++ b/src/Command/App/TaskWaitCommand.php
@@ -1,6 +1,6 @@
addArgument('notification-uuid', InputArgument::REQUIRED, 'The task notification UUID or Cloud Platform API response containing a linked notification')
- ->setHelp('Accepts either a notification UUID or Cloud Platform API response as JSON string. The JSON string must contain the _links->notification->href property.')
- ->addUsage('"$(acli api:environments:domain-clear-caches [environmentId] [domain])"');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $notificationUuid = $input->getArgument('notification-uuid');
- $success = $this->waitForNotificationToComplete($this->cloudApiClientService->getClient(), $notificationUuid, "Waiting for task $notificationUuid to complete");
- return $success ? Command::SUCCESS : Command::FAILURE;
- }
+final class TaskWaitCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('notification-uuid', InputArgument::REQUIRED, 'The task notification UUID or Cloud Platform API response containing a linked notification')
+ ->setHelp('Accepts either a notification UUID or Cloud Platform API response as JSON string. The JSON string must contain the _links->notification->href property.')
+ ->addUsage('"$(acli api:environments:domain-clear-caches [environmentId] [domain])"');
+ }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $notificationUuid = $input->getArgument('notification-uuid');
+ $success = $this->waitForNotificationToComplete($this->cloudApiClientService->getClient(), $notificationUuid, "Waiting for task $notificationUuid to complete");
+ return $success ? Command::SUCCESS : Command::FAILURE;
+ }
}
diff --git a/src/Command/App/UnlinkCommand.php b/src/Command/App/UnlinkCommand.php
index d7f922bb7..f2d07ae20 100644
--- a/src/Command/App/UnlinkCommand.php
+++ b/src/Command/App/UnlinkCommand.php
@@ -1,6 +1,6 @@
validateCwdIsValidDrupalProject();
-
- $projectDir = $this->projectDir;
- if (!$this->getCloudUuidFromDatastore()) {
- throw new AcquiaCliException('There is no Cloud Platform application linked to {projectDir}', ['projectDir' => $projectDir]);
+final class UnlinkCommand extends CommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->validateCwdIsValidDrupalProject();
+
+ $projectDir = $this->projectDir;
+ if (!$this->getCloudUuidFromDatastore()) {
+ throw new AcquiaCliException('There is no Cloud Platform application linked to {projectDir}', ['projectDir' => $projectDir]);
+ }
+
+ $application = $this->getCloudApplication($this->datastoreAcli->get('cloud_app_uuid'));
+ $this->datastoreAcli->set('cloud_app_uuid', null);
+ $output->writeln("Unlinked $projectDir> from Cloud application {$application->name}>");
+
+ return Command::SUCCESS;
}
-
- $application = $this->getCloudApplication($this->datastoreAcli->get('cloud_app_uuid'));
- $this->datastoreAcli->set('cloud_app_uuid', NULL);
- $output->writeln("Unlinked $projectDir> from Cloud application {$application->name}>");
-
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Archive/ArchiveExportCommand.php b/src/Command/Archive/ArchiveExportCommand.php
index e26f52301..46a786d7e 100644
--- a/src/Command/Archive/ArchiveExportCommand.php
+++ b/src/Command/Archive/ArchiveExportCommand.php
@@ -1,6 +1,6 @@
addArgument('destination-dir', InputArgument::REQUIRED, 'The destination directory for the archive file')
- ->addOption('source-dir', 'dir', InputOption::VALUE_REQUIRED, 'The directory containing the Drupal project to be pushed')
- ->addOption('no-files', NULL, InputOption::VALUE_NONE, 'Exclude public files directory from archive')
- ->addOption('no-database', 'no-db', InputOption::VALUE_NONE, 'Exclude database dump from archive');
- }
-
- protected function initialize(InputInterface $input, OutputInterface $output): void {
- parent::initialize($input, $output);
- $this->fs = $this->localMachineHelper->getFilesystem();
- $this->checklist = new Checklist($output);
- $this->setDirAndRequireProjectCwd($input);
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->determineDestinationDir($input);
- $outputCallback = $this->getOutputCallback($output, $this->checklist);
-
- $randomString = (string) random_int(10000, 100000);
- $tempDirName = 'acli-archive-' . basename($this->dir) . '-' . time() . '-' . $randomString;
- $archiveTempDir = Path::join(sys_get_temp_dir(), $tempDirName);
- $this->io->confirm("This will generate a new archive in {$this->destinationDir}> containing the contents of your Drupal application at {$this->dir}>.\n Do you want to continue?");
-
- $this->checklist->addItem('Removing temporary artifact directory');
- $this->checklist->updateProgressBar("Removing $archiveTempDir");
- $this->fs->remove($archiveTempDir);
- $this->fs->mkdir([$archiveTempDir, $archiveTempDir . '/repository']);
- $this->checklist->completePreviousItem();
-
- $this->checklist->addItem('Generating temporary archive directory');
- $this->createArchiveDirectory($archiveTempDir . '/repository');
- $this->checklist->completePreviousItem();
-
- if (!$input->getOption('no-database')) {
- $this->checklist->addItem('Dumping MySQL database');
- $this->exportDatabaseToArchiveDir($outputCallback, $archiveTempDir);
- $this->checklist->completePreviousItem();
+final class ArchiveExportCommand extends CommandBase
+{
+ protected Checklist $checklist;
+
+ private Filesystem $fs;
+
+ /**
+ * @var bool|string|string[]|null
+ */
+ private string|array|bool|null $destinationDir;
+
+ private const PUBLIC_FILES_DIR = '/docroot/sites/default/files';
+
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('destination-dir', InputArgument::REQUIRED, 'The destination directory for the archive file')
+ ->addOption('source-dir', 'dir', InputOption::VALUE_REQUIRED, 'The directory containing the Drupal project to be pushed')
+ ->addOption('no-files', null, InputOption::VALUE_NONE, 'Exclude public files directory from archive')
+ ->addOption('no-database', 'no-db', InputOption::VALUE_NONE, 'Exclude database dump from archive');
}
- $this->checklist->addItem('Compressing archive into a tarball');
- $destinationFilepath = $this->compressArchiveDirectory($archiveTempDir, $this->destinationDir, $outputCallback);
- $outputCallback('out', "Removing $archiveTempDir");
- $this->fs->remove($archiveTempDir);
- $this->checklist->completePreviousItem();
-
- $this->io->newLine();
- $this->io->success("An archive of your Drupal application was created at $destinationFilepath");
- if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
- $this->io->note('You can download the archive through the Cloud IDE user interface by right-clicking the file in your IDE workspace file browser and selecting "Download."');
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ parent::initialize($input, $output);
+ $this->fs = $this->localMachineHelper->getFilesystem();
+ $this->checklist = new Checklist($output);
+ $this->setDirAndRequireProjectCwd($input);
}
- return Command::SUCCESS;
- }
-
- private function determineDestinationDir(InputInterface $input): void {
- $this->destinationDir = $input->getArgument('destination-dir');
- if (!$this->fs->exists($this->destinationDir)) {
- throw new AcquiaCliException("The destination directory {$this->destinationDir} does not exist!");
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->determineDestinationDir($input);
+ $outputCallback = $this->getOutputCallback($output, $this->checklist);
+
+ $randomString = (string) random_int(10000, 100000);
+ $tempDirName = 'acli-archive-' . basename($this->dir) . '-' . time() . '-' . $randomString;
+ $archiveTempDir = Path::join(sys_get_temp_dir(), $tempDirName);
+ $this->io->confirm("This will generate a new archive in {$this->destinationDir}> containing the contents of your Drupal application at {$this->dir}>.\n Do you want to continue?");
+
+ $this->checklist->addItem('Removing temporary artifact directory');
+ $this->checklist->updateProgressBar("Removing $archiveTempDir");
+ $this->fs->remove($archiveTempDir);
+ $this->fs->mkdir([$archiveTempDir, $archiveTempDir . '/repository']);
+ $this->checklist->completePreviousItem();
+
+ $this->checklist->addItem('Generating temporary archive directory');
+ $this->createArchiveDirectory($archiveTempDir . '/repository');
+ $this->checklist->completePreviousItem();
+
+ if (!$input->getOption('no-database')) {
+ $this->checklist->addItem('Dumping MySQL database');
+ $this->exportDatabaseToArchiveDir($outputCallback, $archiveTempDir);
+ $this->checklist->completePreviousItem();
+ }
+
+ $this->checklist->addItem('Compressing archive into a tarball');
+ $destinationFilepath = $this->compressArchiveDirectory($archiveTempDir, $this->destinationDir, $outputCallback);
+ $outputCallback('out', "Removing $archiveTempDir");
+ $this->fs->remove($archiveTempDir);
+ $this->checklist->completePreviousItem();
+
+ $this->io->newLine();
+ $this->io->success("An archive of your Drupal application was created at $destinationFilepath");
+ if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
+ $this->io->note('You can download the archive through the Cloud IDE user interface by right-clicking the file in your IDE workspace file browser and selecting "Download."');
+ }
+
+ return Command::SUCCESS;
}
- }
-
- /**
- * Build the artifact.
- */
- private function createArchiveDirectory(string $artifactDir): void {
- $this->checklist->updateProgressBar("Mirroring source files from {$this->dir} to {$artifactDir}");
- $originFinder = $this->localMachineHelper->getFinder();
- $originFinder->files()->in($this->dir)
- // Include dot files like .htaccess.
- ->ignoreDotFiles(FALSE)
- // If .gitignore exists, ignore VCS files like vendor.
- ->ignoreVCSIgnored(file_exists(Path::join($this->dir, '.gitignore')));
- if ($this->input->getOption('no-files')) {
- $this->checklist->updateProgressBar( 'Skipping ' . self::PUBLIC_FILES_DIR);
- $originFinder->exclude([self::PUBLIC_FILES_DIR]);
+
+ private function determineDestinationDir(InputInterface $input): void
+ {
+ $this->destinationDir = $input->getArgument('destination-dir');
+ if (!$this->fs->exists($this->destinationDir)) {
+ throw new AcquiaCliException("The destination directory {$this->destinationDir} does not exist!");
+ }
}
- $targetFinder = $this->localMachineHelper->getFinder();
- $targetFinder->files()->in($artifactDir)->ignoreDotFiles(FALSE);
- $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => TRUE, 'delete' => TRUE], $targetFinder);
- }
-
- private function exportDatabaseToArchiveDir(
- Closure $outputCallback,
- string $archiveTempDir
- ): void {
- if (!$this->getDrushDatabaseConnectionStatus($outputCallback)) {
- throw new AcquiaCliException("Could not connect to local database.");
+
+ /**
+ * Build the artifact.
+ */
+ private function createArchiveDirectory(string $artifactDir): void
+ {
+ $this->checklist->updateProgressBar("Mirroring source files from {$this->dir} to {$artifactDir}");
+ $originFinder = $this->localMachineHelper->getFinder();
+ $originFinder->files()->in($this->dir)
+ // Include dot files like .htaccess.
+ ->ignoreDotFiles(false)
+ // If .gitignore exists, ignore VCS files like vendor.
+ ->ignoreVCSIgnored(file_exists(Path::join($this->dir, '.gitignore')));
+ if ($this->input->getOption('no-files')) {
+ $this->checklist->updateProgressBar('Skipping ' . self::PUBLIC_FILES_DIR);
+ $originFinder->exclude([self::PUBLIC_FILES_DIR]);
+ }
+ $targetFinder = $this->localMachineHelper->getFinder();
+ $targetFinder->files()->in($artifactDir)->ignoreDotFiles(false);
+ $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => true, 'delete' => true], $targetFinder);
}
- $dumpTempFilepath = $this->createMySqlDumpOnLocal(
- $this->getLocalDbHost(),
- $this->getLocalDbUser(),
- $this->getLocalDbName(),
- $this->getLocalDbPassword(),
- $outputCallback
- );
- $dumpFilepath = Path::join($archiveTempDir, basename($dumpTempFilepath));
- $this->checklist->updateProgressBar("Moving MySQL dump to $dumpFilepath");
- $this->fs->rename($dumpTempFilepath, $dumpFilepath);
- }
-
- private function compressArchiveDirectory(string $archiveDir, string|bool|array|null $destinationDir, Closure $outputCallback = NULL): string {
- $destinationFilename = basename($archiveDir) . '.tar.gz';
- $destinationFilepath = Path::join($destinationDir, $destinationFilename);
- $this->localMachineHelper->checkRequiredBinariesExist(['tar']);
- $process = $this->localMachineHelper->execute(['tar', '-zcvf', $destinationFilepath, '--directory', $archiveDir, '.'], $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to create tarball: {message}', ['message' => $process->getErrorOutput()]);
+
+ private function exportDatabaseToArchiveDir(
+ Closure $outputCallback,
+ string $archiveTempDir
+ ): void {
+ if (!$this->getDrushDatabaseConnectionStatus($outputCallback)) {
+ throw new AcquiaCliException("Could not connect to local database.");
+ }
+ $dumpTempFilepath = $this->createMySqlDumpOnLocal(
+ $this->getLocalDbHost(),
+ $this->getLocalDbUser(),
+ $this->getLocalDbName(),
+ $this->getLocalDbPassword(),
+ $outputCallback
+ );
+ $dumpFilepath = Path::join($archiveTempDir, basename($dumpTempFilepath));
+ $this->checklist->updateProgressBar("Moving MySQL dump to $dumpFilepath");
+ $this->fs->rename($dumpTempFilepath, $dumpFilepath);
}
- return $destinationFilepath;
- }
+ private function compressArchiveDirectory(string $archiveDir, string|bool|array|null $destinationDir, Closure $outputCallback = null): string
+ {
+ $destinationFilename = basename($archiveDir) . '.tar.gz';
+ $destinationFilepath = Path::join($destinationDir, $destinationFilename);
+ $this->localMachineHelper->checkRequiredBinariesExist(['tar']);
+ $process = $this->localMachineHelper->execute(['tar', '-zcvf', $destinationFilepath, '--directory', $archiveDir, '.'], $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to create tarball: {message}', ['message' => $process->getErrorOutput()]);
+ }
+ return $destinationFilepath;
+ }
}
diff --git a/src/Command/Auth/AuthAcsfLoginCommand.php b/src/Command/Auth/AuthAcsfLoginCommand.php
index 25d865812..8e424efe2 100644
--- a/src/Command/Auth/AuthAcsfLoginCommand.php
+++ b/src/Command/Auth/AuthAcsfLoginCommand.php
@@ -1,6 +1,6 @@
addOption('username', 'u', InputOption::VALUE_REQUIRED, "Your Site Factory username")
- ->addOption('key', 'k', InputOption::VALUE_REQUIRED, "Your Site Factory key")
- ->addOption('factory-url', 'f', InputOption::VALUE_REQUIRED, "Your Site Factory URL (including https://)");
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- if ($input->getOption('factory-url')) {
- $factoryUrl = $input->getOption('factory-url');
+final class AuthAcsfLoginCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('username', 'u', InputOption::VALUE_REQUIRED, "Your Site Factory username")
+ ->addOption('key', 'k', InputOption::VALUE_REQUIRED, "Your Site Factory key")
+ ->addOption('factory-url', 'f', InputOption::VALUE_REQUIRED, "Your Site Factory URL (including https://)");
}
- elseif ($input->isInteractive() && $this->datastoreCloud->get('acsf_factories')) {
- $factories = $this->datastoreCloud->get('acsf_factories');
- $factoryChoices = $factories;
- foreach ($factoryChoices as $url => $factoryChoice) {
- $factoryChoices[$url]['url'] = $url;
- }
- $factoryChoices['add_new'] = [
- 'url' => 'Enter a new factory URL',
- ];
- $factory = $this->promptChooseFromObjectsOrArrays($factoryChoices, 'url', 'url', 'Choose a Factory to login to');
- if ($factory['url'] === 'Enter a new factory URL') {
- $factoryUrl = $this->io->ask('Enter the full URL of the factory');
- $factory = [
- 'url' => $factoryUrl,
- 'users' => [],
- ];
- }
- else {
- $factoryUrl = $factory['url'];
- }
- $users = $factory['users'];
- $users['add_new'] = [
- 'username' => 'Enter a new user',
- ];
- $selectedUser = $this->promptChooseFromObjectsOrArrays($users, 'username', 'username', 'Choose which user to login as');
- if ($selectedUser['username'] !== 'Enter a new user') {
- $this->datastoreCloud->set('acsf_active_factory', $factoryUrl);
- $factories[$factoryUrl]['active_user'] = $selectedUser['username'];
- $this->datastoreCloud->set('acsf_factories', $factories);
- $output->writeln([
- "Acquia CLI is now logged in to {$factory['url']}> as {$selectedUser['username']}>",
- ]);
- return Command::SUCCESS;
- }
- }
- else {
- $factoryUrl = $this->determineOption('factory-url');
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if ($input->getOption('factory-url')) {
+ $factoryUrl = $input->getOption('factory-url');
+ } elseif ($input->isInteractive() && $this->datastoreCloud->get('acsf_factories')) {
+ $factories = $this->datastoreCloud->get('acsf_factories');
+ $factoryChoices = $factories;
+ foreach ($factoryChoices as $url => $factoryChoice) {
+ $factoryChoices[$url]['url'] = $url;
+ }
+ $factoryChoices['add_new'] = [
+ 'url' => 'Enter a new factory URL',
+ ];
+ $factory = $this->promptChooseFromObjectsOrArrays($factoryChoices, 'url', 'url', 'Choose a Factory to login to');
+ if ($factory['url'] === 'Enter a new factory URL') {
+ $factoryUrl = $this->io->ask('Enter the full URL of the factory');
+ $factory = [
+ 'url' => $factoryUrl,
+ 'users' => [],
+ ];
+ } else {
+ $factoryUrl = $factory['url'];
+ }
- $username = $this->determineOption('username');
- $key = $this->determineOption('key', TRUE);
+ $users = $factory['users'];
+ $users['add_new'] = [
+ 'username' => 'Enter a new user',
+ ];
+ $selectedUser = $this->promptChooseFromObjectsOrArrays($users, 'username', 'username', 'Choose which user to login as');
+ if ($selectedUser['username'] !== 'Enter a new user') {
+ $this->datastoreCloud->set('acsf_active_factory', $factoryUrl);
+ $factories[$factoryUrl]['active_user'] = $selectedUser['username'];
+ $this->datastoreCloud->set('acsf_factories', $factories);
+ $output->writeln([
+ "Acquia CLI is now logged in to {$factory['url']}> as {$selectedUser['username']}>",
+ ]);
+ return Command::SUCCESS;
+ }
+ } else {
+ $factoryUrl = $this->determineOption('factory-url');
+ }
- $this->writeAcsfCredentialsToDisk($factoryUrl, $username, $key);
- $output->writeln("Saved credentials");
+ $username = $this->determineOption('username');
+ $key = $this->determineOption('key', true);
- return Command::SUCCESS;
- }
+ $this->writeAcsfCredentialsToDisk($factoryUrl, $username, $key);
+ $output->writeln("Saved credentials");
- private function writeAcsfCredentialsToDisk(?string $factoryUrl, string $username, string $key): void {
- $keys = $this->datastoreCloud->get('acsf_factories');
- $keys[$factoryUrl]['users'][$username] = [
- 'key' => $key,
- 'username' => $username,
- ];
- $keys[$factoryUrl]['url'] = $factoryUrl;
- $keys[$factoryUrl]['active_user'] = $username;
- $this->datastoreCloud->set('acsf_factories', $keys);
- $this->datastoreCloud->set('acsf_active_factory', $factoryUrl);
- }
+ return Command::SUCCESS;
+ }
+ private function writeAcsfCredentialsToDisk(?string $factoryUrl, string $username, string $key): void
+ {
+ $keys = $this->datastoreCloud->get('acsf_factories');
+ $keys[$factoryUrl]['users'][$username] = [
+ 'key' => $key,
+ 'username' => $username,
+ ];
+ $keys[$factoryUrl]['url'] = $factoryUrl;
+ $keys[$factoryUrl]['active_user'] = $username;
+ $this->datastoreCloud->set('acsf_factories', $keys);
+ $this->datastoreCloud->set('acsf_active_factory', $factoryUrl);
+ }
}
diff --git a/src/Command/Auth/AuthAcsfLogoutCommand.php b/src/Command/Auth/AuthAcsfLogoutCommand.php
index 910018958..8d84c9fe6 100644
--- a/src/Command/Auth/AuthAcsfLogoutCommand.php
+++ b/src/Command/Auth/AuthAcsfLogoutCommand.php
@@ -1,6 +1,6 @@
datastoreCloud->get('acsf_factories');
- if (empty($factories)) {
- $this->io->error(['You are not logged into any factories.']);
- return Command::FAILURE;
- }
- foreach ($factories as $url => $factory) {
- $factories[$url]['url'] = $url;
- }
- $factory = $this->promptChooseFromObjectsOrArrays($factories, 'url', 'url', 'Choose a Factory to logout of');
- $factoryUrl = $factory['url'];
-
- /** @var \Acquia\Cli\AcsfApi\AcsfCredentials $cloudCredentials */
- $cloudCredentials = $this->cloudCredentials;
- $activeUser = $cloudCredentials->getFactoryActiveUser($factory);
- // @todo Only show factories the user is logged into.
- if (!$activeUser) {
- $this->io->error("You're already logged out of $factoryUrl");
- return 1;
+final class AuthAcsfLogoutCommand extends CommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $factories = $this->datastoreCloud->get('acsf_factories');
+ if (empty($factories)) {
+ $this->io->error(['You are not logged into any factories.']);
+ return Command::FAILURE;
+ }
+ foreach ($factories as $url => $factory) {
+ $factories[$url]['url'] = $url;
+ }
+ $factory = $this->promptChooseFromObjectsOrArrays($factories, 'url', 'url', 'Choose a Factory to logout of');
+ $factoryUrl = $factory['url'];
+
+ /** @var \Acquia\Cli\AcsfApi\AcsfCredentials $cloudCredentials */
+ $cloudCredentials = $this->cloudCredentials;
+ $activeUser = $cloudCredentials->getFactoryActiveUser($factory);
+ // @todo Only show factories the user is logged into.
+ if (!$activeUser) {
+ $this->io->error("You're already logged out of $factoryUrl");
+ return 1;
+ }
+ $answer = $this->io->confirm("Are you sure you'd like to logout the user {$activeUser['username']} from $factoryUrl?");
+ if (!$answer) {
+ return Command::SUCCESS;
+ }
+ $factories[$factoryUrl]['active_user'] = null;
+ $this->datastoreCloud->set('acsf_factories', $factories);
+ $this->datastoreCloud->remove('acsf_active_factory');
+
+ $output->writeln("Logged {$activeUser['username']} out of $factoryUrl");
+
+ return Command::SUCCESS;
}
- $answer = $this->io->confirm("Are you sure you'd like to logout the user {$activeUser['username']} from $factoryUrl?");
- if (!$answer) {
- return Command::SUCCESS;
- }
- $factories[$factoryUrl]['active_user'] = NULL;
- $this->datastoreCloud->set('acsf_factories', $factories);
- $this->datastoreCloud->remove('acsf_active_factory');
-
- $output->writeln("Logged {$activeUser['username']} out of $factoryUrl");
-
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Auth/AuthLoginCommand.php b/src/Command/Auth/AuthLoginCommand.php
index 580262308..ba8274741 100644
--- a/src/Command/Auth/AuthLoginCommand.php
+++ b/src/Command/Auth/AuthLoginCommand.php
@@ -1,6 +1,6 @@
addOption('key', 'k', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API key')
- ->addOption('secret', 's', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API secret')
- ->setHelp('Acquia CLI can store multiple sets of credentials in case you have multiple Cloud Platform accounts. However, only a single account can be active at a time. This command allows you to activate a new or existing set of credentials.');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $keys = $this->datastoreCloud->get('keys');
- $activeKey = $this->datastoreCloud->get('acli_key');
- if ($activeKey) {
- $activeKeyLabel = $keys[$activeKey]['label'];
- $output->writeln("The following Cloud Platform API key is active: $activeKeyLabel>");
- }
- else {
- $output->writeln('No Cloud Platform API key is active');
+final class AuthLoginCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('key', 'k', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API key')
+ ->addOption('secret', 's', InputOption::VALUE_REQUIRED, 'Your Cloud Platform API secret')
+ ->setHelp('Acquia CLI can store multiple sets of credentials in case you have multiple Cloud Platform accounts. However, only a single account can be active at a time. This command allows you to activate a new or existing set of credentials.');
}
- // If keys already are saved locally, prompt to select.
- if ($keys && $input->isInteractive()) {
- foreach ($keys as $uuid => $key) {
- $keys[$uuid]['uuid'] = $uuid;
- }
- $keys['create_new'] = [
- 'label' => 'Enter a new API key',
- 'uuid' => 'create_new',
- ];
- $selectedKey = $this->promptChooseFromObjectsOrArrays($keys, 'uuid', 'label', 'Activate a Cloud Platform API key');
- if ($selectedKey['uuid'] !== 'create_new') {
- $this->datastoreCloud->set('acli_key', $selectedKey['uuid']);
- $output->writeln("Acquia CLI will use the API key {$selectedKey['label']}>");
- $this->reAuthenticate($this->cloudCredentials->getCloudKey(), $this->cloudCredentials->getCloudSecret(), $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
- return Command::SUCCESS;
- }
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $keys = $this->datastoreCloud->get('keys');
+ $activeKey = $this->datastoreCloud->get('acli_key');
+ if ($activeKey) {
+ $activeKeyLabel = $keys[$activeKey]['label'];
+ $output->writeln("The following Cloud Platform API key is active: $activeKeyLabel>");
+ } else {
+ $output->writeln('No Cloud Platform API key is active');
+ }
- $this->promptOpenBrowserToCreateToken($input);
- $apiKey = $this->determineApiKey();
- $apiSecret = $this->determineApiSecret();
- $this->reAuthenticate($apiKey, $apiSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
- $this->writeApiCredentialsToDisk($apiKey, $apiSecret);
- $output->writeln("Saved credentials");
+ // If keys already are saved locally, prompt to select.
+ if ($keys && $input->isInteractive()) {
+ foreach ($keys as $uuid => $key) {
+ $keys[$uuid]['uuid'] = $uuid;
+ }
+ $keys['create_new'] = [
+ 'label' => 'Enter a new API key',
+ 'uuid' => 'create_new',
+ ];
+ $selectedKey = $this->promptChooseFromObjectsOrArrays($keys, 'uuid', 'label', 'Activate a Cloud Platform API key');
+ if ($selectedKey['uuid'] !== 'create_new') {
+ $this->datastoreCloud->set('acli_key', $selectedKey['uuid']);
+ $output->writeln("Acquia CLI will use the API key {$selectedKey['label']}>");
+ $this->reAuthenticate($this->cloudCredentials->getCloudKey(), $this->cloudCredentials->getCloudSecret(), $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
+ return Command::SUCCESS;
+ }
+ }
- return Command::SUCCESS;
- }
+ $this->promptOpenBrowserToCreateToken($input);
+ $apiKey = $this->determineApiKey();
+ $apiSecret = $this->determineApiSecret();
+ $this->reAuthenticate($apiKey, $apiSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
+ $this->writeApiCredentialsToDisk($apiKey, $apiSecret);
+ $output->writeln("Saved credentials");
- private function writeApiCredentialsToDisk(string $apiKey, string $apiSecret): void {
- $account = new Account($this->cloudApiClientService->getClient());
- $accountInfo = $account->get();
- $keys = $this->datastoreCloud->get('keys');
- $keys[$apiKey] = [
- 'label' => $accountInfo->mail,
- 'secret' => $apiSecret,
- 'uuid' => $apiKey,
- ];
- $this->datastoreCloud->set('keys', $keys);
- $this->datastoreCloud->set('acli_key', $apiKey);
- }
+ return Command::SUCCESS;
+ }
+ private function writeApiCredentialsToDisk(string $apiKey, string $apiSecret): void
+ {
+ $account = new Account($this->cloudApiClientService->getClient());
+ $accountInfo = $account->get();
+ $keys = $this->datastoreCloud->get('keys');
+ $keys[$apiKey] = [
+ 'label' => $accountInfo->mail,
+ 'secret' => $apiSecret,
+ 'uuid' => $apiKey,
+ ];
+ $this->datastoreCloud->set('keys', $keys);
+ $this->datastoreCloud->set('acli_key', $apiKey);
+ }
}
diff --git a/src/Command/Auth/AuthLogoutCommand.php b/src/Command/Auth/AuthLogoutCommand.php
index e498709fc..35e48f431 100644
--- a/src/Command/Auth/AuthLogoutCommand.php
+++ b/src/Command/Auth/AuthLogoutCommand.php
@@ -1,6 +1,6 @@
addOption('delete', NULL, InputOption::VALUE_NEGATABLE, 'Delete the active Cloud Platform API credentials');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $keys = $this->datastoreCloud->get('keys');
- $activeKey = $this->datastoreCloud->get('acli_key');
- if (!$activeKey) {
- throw new AcquiaCliException('There is no active Cloud Platform API key');
- }
- $activeKeyLabel = $keys[$activeKey]['label'];
- $output->writeln("The key $activeKeyLabel> will be deactivated on this machine. However, the credentials will remain on disk and can be reactivated by running acli auth:login> unless you also choose to delete them.");
- $delete = $this->determineOption('delete', FALSE, NULL, NULL, FALSE);
- $this->datastoreCloud->remove('acli_key');
- $action = 'deactivated';
- if ($delete) {
- $this->datastoreCloud->remove("keys.$activeKey");
- $action = 'deleted';
+final class AuthLogoutCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this->addOption('delete', null, InputOption::VALUE_NEGATABLE, 'Delete the active Cloud Platform API credentials');
}
- $output->writeln("The active Cloud Platform API credentials were $action");
- $output->writeln('No Cloud Platform API key is active. Run acli auth:login> to continue using the Cloud Platform API.');
-
- return Command::SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $keys = $this->datastoreCloud->get('keys');
+ $activeKey = $this->datastoreCloud->get('acli_key');
+ if (!$activeKey) {
+ throw new AcquiaCliException('There is no active Cloud Platform API key');
+ }
+ $activeKeyLabel = $keys[$activeKey]['label'];
+ $output->writeln("The key $activeKeyLabel> will be deactivated on this machine. However, the credentials will remain on disk and can be reactivated by running acli auth:login> unless you also choose to delete them.");
+ $delete = $this->determineOption('delete', false, null, null, false);
+ $this->datastoreCloud->remove('acli_key');
+ $action = 'deactivated';
+ if ($delete) {
+ $this->datastoreCloud->remove("keys.$activeKey");
+ $action = 'deleted';
+ }
+ $output->writeln("The active Cloud Platform API credentials were $action");
+ $output->writeln('No Cloud Platform API key is active. Run acli auth:login> to continue using the Cloud Platform API.');
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/CodeStudio/CodeStudioCiCdVariables.php b/src/Command/CodeStudio/CodeStudioCiCdVariables.php
index de7691aac..d6f01d174 100644
--- a/src/Command/CodeStudio/CodeStudioCiCdVariables.php
+++ b/src/Command/CodeStudio/CodeStudioCiCdVariables.php
@@ -1,117 +1,119 @@
+ */
+ public static function getList(): array
+ {
+ // Getlist is being utilised in pipeline-migrate command. By default command is supporting drupal project but going forward need to support both drupal and nodejs project.
+ return array_column(self::getDefaultsForPhp(), 'key');
+ }
- /**
- * @return array
- */
- public static function getList(): array {
- // Getlist is being utilised in pipeline-migrate command. By default command is supporting drupal project but going forward need to support both drupal and nodejs project.
- return array_column(self::getDefaultsForPhp(), 'key');
- }
-
- /**
- * @return array
- */
- public static function getDefaultsForNode(?string $cloudApplicationUuid = NULL, ?string $cloudKey = NULL, ?string $cloudSecret = NULL, ?string $projectAccessTokenName = NULL, ?string $projectAccessToken = NULL, ?string $nodeVersion = NULL): array {
- return [
- [
+ /**
+ * @return array
+ */
+ public static function getDefaultsForNode(?string $cloudApplicationUuid = null, ?string $cloudKey = null, ?string $cloudSecret = null, ?string $projectAccessTokenName = null, ?string $projectAccessToken = null, ?string $nodeVersion = null): array
+ {
+ return [
+ [
'key' => 'ACQUIA_APPLICATION_UUID',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $cloudApplicationUuid,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $cloudKey,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $cloudSecret,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_GLAB_TOKEN_NAME',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $projectAccessTokenName,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_GLAB_TOKEN_SECRET',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $projectAccessToken,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'NODE_VERSION',
- 'masked' => FALSE,
- 'protected' => FALSE,
+ 'masked' => false,
+ 'protected' => false,
'value' => $nodeVersion,
'variable_type' => 'env_var',
- ],
- ];
- }
+ ],
+ ];
+ }
- /**
- * @return array
- */
- public static function getDefaultsForPhp(?string $cloudApplicationUuid = NULL, ?string $cloudKey = NULL, ?string $cloudSecret = NULL, ?string $projectAccessTokenName = NULL, ?string $projectAccessToken = NULL, ?string $phpVersion = NULL): array {
- return [
- [
+ /**
+ * @return array
+ */
+ public static function getDefaultsForPhp(?string $cloudApplicationUuid = null, ?string $cloudKey = null, ?string $cloudSecret = null, ?string $projectAccessTokenName = null, ?string $projectAccessToken = null, ?string $phpVersion = null): array
+ {
+ return [
+ [
'key' => 'ACQUIA_APPLICATION_UUID',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $cloudApplicationUuid,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_CLOUD_API_TOKEN_KEY',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $cloudKey,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_CLOUD_API_TOKEN_SECRET',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $cloudSecret,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_GLAB_TOKEN_NAME',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $projectAccessTokenName,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'ACQUIA_GLAB_TOKEN_SECRET',
- 'masked' => TRUE,
- 'protected' => FALSE,
+ 'masked' => true,
+ 'protected' => false,
'value' => $projectAccessToken,
'variable_type' => 'env_var',
- ],
- [
+ ],
+ [
'key' => 'PHP_VERSION',
- 'masked' => FALSE,
- 'protected' => FALSE,
+ 'masked' => false,
+ 'protected' => false,
'value' => $phpVersion,
'variable_type' => 'env_var',
- ],
- ];
- }
-
+ ],
+ ];
+ }
}
diff --git a/src/Command/CodeStudio/CodeStudioCommandTrait.php b/src/Command/CodeStudio/CodeStudioCommandTrait.php
index b137a147e..fb7dfe7d9 100644
--- a/src/Command/CodeStudio/CodeStudioCommandTrait.php
+++ b/src/Command/CodeStudio/CodeStudioCommandTrait.php
@@ -1,6 +1,6 @@
+ */
+ protected array $gitLabAccount;
- /**
- * @var array
- */
- protected array $gitLabAccount;
+ private string $gitLabProjectDescription;
- private string $gitLabProjectDescription;
+ /**
+ * Getting the gitlab token from user.
+ */
+ private function getGitLabToken(string $gitlabHost): string
+ {
+ if ($this->input->getOption('gitlab-token')) {
+ return $this->input->getOption('gitlab-token');
+ }
+ if (!$this->localMachineHelper->commandExists('glab')) {
+ throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation");
+ }
+ $process = $this->localMachineHelper->execute([
+ 'glab',
+ 'config',
+ 'get',
+ 'token',
+ '--host=' . $gitlabHost,
+ ], null, null, false);
+ if ($process->isSuccessful() && trim($process->getOutput())) {
+ return trim($process->getOutput());
+ }
- /**
- * Getting the gitlab token from user.
- */
- private function getGitLabToken(string $gitlabHost): string {
- if ($this->input->getOption('gitlab-token')) {
- return $this->input->getOption('gitlab-token');
- }
- if (!$this->localMachineHelper->commandExists('glab')) {
- throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation");
- }
- $process = $this->localMachineHelper->execute([
- 'glab',
- 'config',
- 'get',
- 'token',
- '--host=' . $gitlabHost,
- ], NULL, NULL, FALSE);
- if ($process->isSuccessful() && trim($process->getOutput())) {
- return trim($process->getOutput());
- }
+ $this->io->writeln([
+ "",
+ "You must first authenticate with Code Studio by creating a personal access token:",
+ "* Visit https://$gitlabHost/-/profile/personal_access_tokens",
+ "* Create a token and grant it both api and write repository scopes",
+ "* Copy the token to your clipboard",
+ "* Run glab auth login --hostname=$gitlabHost and paste the token when prompted",
+ "* Try this command again.",
+ ]);
- $this->io->writeln([
- "",
- "You must first authenticate with Code Studio by creating a personal access token:",
- "* Visit https://$gitlabHost/-/profile/personal_access_tokens",
- "* Create a token and grant it both api and write repository scopes",
- "* Copy the token to your clipboard",
- "* Run glab auth login --hostname=$gitlabHost and paste the token when prompted",
- "* Try this command again.",
- ]);
+ throw new AcquiaCliException("Could not determine GitLab token");
+ }
- throw new AcquiaCliException("Could not determine GitLab token");
- }
+ /**
+ * Getting gitlab host from user.
+ */
+ private function getGitLabHost(): string
+ {
+ // If hostname is available as argument, use that.
+ if (
+ $this->input->hasOption('gitlab-host-name')
+ && $this->input->getOption('gitlab-host-name')
+ ) {
+ return $this->input->getOption('gitlab-host-name');
+ }
+ if (!$this->localMachineHelper->commandExists('glab')) {
+ throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation");
+ }
+ $process = $this->localMachineHelper->execute([
+ 'glab',
+ 'config',
+ 'get',
+ 'host',
+ ], null, null, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Could not determine GitLab host: {error_message}", ['error_message' => $process->getErrorOutput()]);
+ }
+ $output = trim($process->getOutput());
+ $urlParts = parse_url($output);
+ if (!array_key_exists('scheme', $urlParts) && !array_key_exists('host', $urlParts)) {
+ // $output looks like code.cloudservices.acquia.io.
+ return $output;
+ }
+ // $output looks like http://code.cloudservices.acquia.io/.
+ return $urlParts['host'];
+ }
- /**
- * Getting gitlab host from user.
- */
- private function getGitLabHost(): string {
- // If hostname is available as argument, use that.
- if ($this->input->hasOption('gitlab-host-name')
- && $this->input->getOption('gitlab-host-name')) {
- return $this->input->getOption('gitlab-host-name');
+ private function getGitLabClient(): Client
+ {
+ if (!isset($this->gitLabClient)) {
+ $gitlabClient = new Client(new Builder(new \GuzzleHttp\Client()));
+ $gitlabClient->setUrl('https://' . $this->gitLabHost);
+ $gitlabClient->authenticate($this->gitLabToken, Client::AUTH_OAUTH_TOKEN);
+ $this->setGitLabClient($gitlabClient);
+ }
+ return $this->gitLabClient;
}
- if (!$this->localMachineHelper->commandExists('glab')) {
- throw new AcquiaCliException("Install glab to continue: https://gitlab.com/gitlab-org/cli#installation");
+
+ public function setGitLabClient(Client $client): void
+ {
+ $this->gitLabClient = $client;
}
- $process = $this->localMachineHelper->execute([
- 'glab',
- 'config',
- 'get',
- 'host',
- ], NULL, NULL, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Could not determine GitLab host: {error_message}", ['error_message' => $process->getErrorOutput()]);
+
+ private function writeApiTokenMessage(InputInterface $input): void
+ {
+ // Get Cloud access tokens.
+ if (!$input->getOption('key') || !$input->getOption('secret')) {
+ $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens';
+ $this->io->writeln([
+ "",
+ "This will configure AutoDevOps for a Code Studio project using credentials",
+ "(an API Token and SSH Key) belonging to your current Acquia Cloud Platform user account.",
+ "Before continuing, make sure that you're logged into the right Acquia Cloud Platform user account.",
+ "",
+ "Typically this command should only be run once per application",
+ "but if your Cloud Platform account is deleted in the future, the Code Studio project will",
+ "need to be re-configured using a different user account.",
+ "",
+ "To begin, visit this URL and create a new API Token for Code Studio to use:>",
+ "$tokenUrl>",
+ ]);
+ }
}
- $output = trim($process->getOutput());
- $urlParts = parse_url($output);
- if (!array_key_exists('scheme', $urlParts) && !array_key_exists('host', $urlParts)) {
- // $output looks like code.cloudservices.acquia.io.
- return $output;
+
+ protected function validateEnvironment(): void
+ {
+ if (!empty(self::isAcquiaCloudIde()) && !getenv('GITLAB_HOST')) {
+ throw new AcquiaCliException('The GITLAB_HOST environment variable must be set or the `--gitlab-host-name` option must be passed.');
+ }
}
- // $output looks like http://code.cloudservices.acquia.io/.
- return $urlParts['host'];
- }
- private function getGitLabClient(): Client {
- if (!isset($this->gitLabClient)) {
- $gitlabClient = new Client(new Builder(new \GuzzleHttp\Client()));
- $gitlabClient->setUrl('https://' . $this->gitLabHost);
- $gitlabClient->authenticate($this->gitLabToken, Client::AUTH_OAUTH_TOKEN);
- $this->setGitLabClient($gitlabClient);
+ private function authenticateWithGitLab(): void
+ {
+ $this->validateEnvironment();
+ $this->gitLabHost = $this->getGitLabHost();
+ $this->gitLabToken = $this->getGitLabToken($this->gitLabHost);
+ $this->getGitLabClient();
+ try {
+ $this->gitLabAccount = $this->gitLabClient->users()->me();
+ } catch (RuntimeException $exception) {
+ $this->io->error([
+ "Unable to authenticate with Code Studio",
+ "Did you set a valid token with the api> and write_repository> scopes?",
+ "Try running `glab auth login` to re-authenticate.",
+ "Alternatively, pass the --gitlab-token> option.",
+ "Then try again.",
+ ]);
+ throw new AcquiaCliException("Unable to authenticate with Code Studio");
+ }
}
- return $this->gitLabClient;
- }
- public function setGitLabClient(Client $client): void {
- $this->gitLabClient = $client;
- }
+ /**
+ * @return array
+ */
+ private function determineGitLabProject(ApplicationResponse $cloudApplication): array
+ {
+ // Use command option.
+ if ($this->input->getOption('gitlab-project-id')) {
+ $id = $this->input->getOption('gitlab-project-id');
+ return $this->gitLabClient->projects()->show($id);
+ }
+ // Search for existing project that matches expected description pattern.
+ $projects = $this->gitLabClient->projects()->all(['search' => $cloudApplication->uuid]);
+ if ($projects) {
+ if (count($projects) == 1) {
+ return reset($projects);
+ }
- private function writeApiTokenMessage(InputInterface $input): void {
- // Get Cloud access tokens.
- if (!$input->getOption('key') || !$input->getOption('secret')) {
- $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens';
- $this->io->writeln([
- "",
- "This will configure AutoDevOps for a Code Studio project using credentials",
- "(an API Token and SSH Key) belonging to your current Acquia Cloud Platform user account.",
- "Before continuing, make sure that you're logged into the right Acquia Cloud Platform user account.",
- "",
- "Typically this command should only be run once per application",
- "but if your Cloud Platform account is deleted in the future, the Code Studio project will",
- "need to be re-configured using a different user account.",
+ return $this->promptChooseFromObjectsOrArrays(
+ $projects,
+ 'id',
+ 'path_with_namespace',
+ "Found multiple projects that could match the {$cloudApplication->name} application. Choose which one to configure."
+ );
+ }
+ // Prompt to create project.
+ $this->io->writeln([
"",
- "To begin, visit this URL and create a new API Token for Code Studio to use:>",
- "$tokenUrl>",
- ]);
+ "Could not find any existing Code Studio project for Acquia Cloud Platform application {$cloudApplication->name}.",
+ "Searched for UUID {$cloudApplication->uuid} in project descriptions.",
+ ]);
+ $createProject = $this->io->confirm('Would you like to create a new Code Studio project? If you select "no" you may choose from a full list of existing projects.');
+ if ($createProject) {
+ return $this->createGitLabProject($cloudApplication);
+ }
+ // Prompt to choose from full list, regardless of description.
+ return $this->promptChooseFromObjectsOrArrays(
+ $this->gitLabClient->projects()->all(),
+ 'id',
+ 'path_with_namespace',
+ "Choose a Code Studio project to configure for the {$cloudApplication->name} application"
+ );
}
- }
- protected function validateEnvironment(): void {
- if (!empty(self::isAcquiaCloudIde()) && !getenv('GITLAB_HOST')) {
- throw new AcquiaCliException('The GITLAB_HOST environment variable must be set or the `--gitlab-host-name` option must be passed.');
- }
- }
+ /**
+ * @return array
+ */
+ private function createGitLabProject(ApplicationResponse $cloudApplication): array
+ {
+ $userGroups = $this->gitLabClient->groups()->all([
+ 'all_available' => true,
+ 'min_access_level' => 40,
+ ]);
+ $parameters = $this->getGitLabProjectDefaults();
+ if ($userGroups) {
+ $userGroups[] = $this->gitLabClient->namespaces()->show($this->gitLabAccount['username']);
+ $projectGroup = $this->promptChooseFromObjectsOrArrays($userGroups, 'id', 'path', 'Choose which group this new project should belong to:');
+ $parameters['namespace_id'] = $projectGroup['id'];
+ }
- private function authenticateWithGitLab(): void {
- $this->validateEnvironment();
- $this->gitLabHost = $this->getGitLabHost();
- $this->gitLabToken = $this->getGitLabToken($this->gitLabHost);
- $this->getGitLabClient();
- try {
- $this->gitLabAccount = $this->gitLabClient->users()->me();
- }
- catch (RuntimeException $exception) {
- $this->io->error([
- "Unable to authenticate with Code Studio",
- "Did you set a valid token with the api> and write_repository> scopes?",
- "Try running `glab auth login` to re-authenticate.",
- "Alternatively, pass the --gitlab-token> option.",
- "Then try again.",
- ]);
- throw new AcquiaCliException("Unable to authenticate with Code Studio");
- }
- }
+ $slugger = new AsciiSlugger();
+ $projectName = (string) $slugger->slug($cloudApplication->name);
+ $project = $this->gitLabClient->projects()->create($projectName, $parameters);
+ try {
+ $this->gitLabClient->projects()
+ ->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png');
+ } catch (ValidationFailedException) {
+ $this->io->warning("Failed to upload project avatar");
+ }
+ $this->io->success("Created {$project['path_with_namespace']} project in Code Studio.");
- /**
- * @return array
- */
- private function determineGitLabProject(ApplicationResponse $cloudApplication): array {
- // Use command option.
- if ($this->input->getOption('gitlab-project-id')) {
- $id = $this->input->getOption('gitlab-project-id');
- return $this->gitLabClient->projects()->show($id);
+ return $project;
}
- // Search for existing project that matches expected description pattern.
- $projects = $this->gitLabClient->projects()->all(['search' => $cloudApplication->uuid]);
- if ($projects) {
- if (count($projects) == 1) {
- return reset($projects);
- }
- return $this->promptChooseFromObjectsOrArrays(
- $projects,
- 'id',
- 'path_with_namespace',
- "Found multiple projects that could match the {$cloudApplication->name} application. Choose which one to configure."
- );
+ private function setGitLabProjectDescription(mixed $cloudApplicationUuid): void
+ {
+ $this->gitLabProjectDescription = "Source repository for Acquia Cloud Platform application $cloudApplicationUuid";
}
- // Prompt to create project.
- $this->io->writeln([
- "",
- "Could not find any existing Code Studio project for Acquia Cloud Platform application {$cloudApplication->name}.",
- "Searched for UUID {$cloudApplication->uuid} in project descriptions.",
- ]);
- $createProject = $this->io->confirm('Would you like to create a new Code Studio project? If you select "no" you may choose from a full list of existing projects.');
- if ($createProject) {
- return $this->createGitLabProject($cloudApplication);
- }
- // Prompt to choose from full list, regardless of description.
- return $this->promptChooseFromObjectsOrArrays(
- $this->gitLabClient->projects()->all(),
- 'id',
- 'path_with_namespace',
- "Choose a Code Studio project to configure for the {$cloudApplication->name} application"
- );
- }
- /**
- * @return array
- */
- private function createGitLabProject(ApplicationResponse $cloudApplication): array {
- $userGroups = $this->gitLabClient->groups()->all([
- 'all_available' => TRUE,
- 'min_access_level' => 40,
- ]);
- $parameters = $this->getGitLabProjectDefaults();
- if ($userGroups) {
- $userGroups[] = $this->gitLabClient->namespaces()->show($this->gitLabAccount['username']);
- $projectGroup = $this->promptChooseFromObjectsOrArrays($userGroups, 'id', 'path', 'Choose which group this new project should belong to:');
- $parameters['namespace_id'] = $projectGroup['id'];
+ /**
+ * @return array
+ */
+ private function getGitLabProjectDefaults(): array
+ {
+ return [
+ 'container_registry_access_level' => 'disabled',
+ 'default_branch' => 'main',
+ 'description' => $this->gitLabProjectDescription,
+ 'initialize_with_readme' => true,
+ 'topics' => 'Acquia Cloud Application',
+ ];
}
- $slugger = new AsciiSlugger();
- $projectName = (string) $slugger->slug($cloudApplication->name);
- $project = $this->gitLabClient->projects()->create($projectName, $parameters);
- try {
- $this->gitLabClient->projects()
- ->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png');
- }
- catch (ValidationFailedException) {
- $this->io->warning("Failed to upload project avatar");
+ /**
+ * Add gitlab options to the command.
+ *
+ * @return $this
+ */
+ private function acceptGitlabOptions(): static
+ {
+ $this->addOption('gitlab-token', null, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance')
+ ->addOption('gitlab-project-id', null, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.')
+ ->addOption('gitlab-host-name', null, InputOption::VALUE_REQUIRED, 'The GitLab hostname.');
+ return $this;
}
- $this->io->success("Created {$project['path_with_namespace']} project in Code Studio.");
-
- return $project;
- }
-
- private function setGitLabProjectDescription(mixed $cloudApplicationUuid): void {
- $this->gitLabProjectDescription = "Source repository for Acquia Cloud Platform application $cloudApplicationUuid";
- }
-
- /**
- * @return array
- */
- private function getGitLabProjectDefaults(): array {
- return [
- 'container_registry_access_level' => 'disabled',
- 'default_branch' => 'main',
- 'description' => $this->gitLabProjectDescription,
- 'initialize_with_readme' => TRUE,
- 'topics' => 'Acquia Cloud Application',
- ];
- }
-
- /**
- * Add gitlab options to the command.
- *
- * @return $this
- */
- private function acceptGitlabOptions(): static {
- $this->addOption('gitlab-token', NULL, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance')
- ->addOption('gitlab-project-id', NULL, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.')
- ->addOption('gitlab-host-name', NULL, InputOption::VALUE_REQUIRED, 'The GitLab hostname.');
- return $this;
- }
-
}
diff --git a/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php b/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php
index 64a9cf69c..537ffcc71 100644
--- a/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php
+++ b/src/Command/CodeStudio/CodeStudioPhpVersionCommand.php
@@ -1,6 +1,6 @@
addArgument('php-version', InputArgument::REQUIRED, 'The PHP version that needs to configured or updated')
+ ->addUsage('8.1 myapp')
+ ->addUsage('8.1 abcd1234-1111-2222-3333-0e02b2c3d470');
+ $this->acceptApplicationUuid();
+ $this->acceptGitlabOptions();
+ }
- protected function configure(): void {
- $this
- ->addArgument('php-version', InputArgument::REQUIRED, 'The PHP version that needs to configured or updated')
- ->addUsage('8.1 myapp')
- ->addUsage('8.1 abcd1234-1111-2222-3333-0e02b2c3d470');
- $this->acceptApplicationUuid();
- $this->acceptGitlabOptions();
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $phpVersion = $input->getArgument('php-version');
+ $this->validatePhpVersion($phpVersion);
+ $this->authenticateWithGitLab();
+ $acquiaCloudAppId = $this->determineCloudApplication();
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $phpVersion = $input->getArgument('php-version');
- $this->validatePhpVersion($phpVersion);
- $this->authenticateWithGitLab();
- $acquiaCloudAppId = $this->determineCloudApplication();
+ // Get the GitLab project details attached with the given Cloud application.
+ $cloudApplication = $this->getCloudApplication($acquiaCloudAppId);
+ $project = $this->determineGitLabProject($cloudApplication);
- // Get the GitLab project details attached with the given Cloud application.
- $cloudApplication = $this->getCloudApplication($acquiaCloudAppId);
- $project = $this->determineGitLabProject($cloudApplication);
+ // If CI/CD is not enabled for the project in Code Studio.
+ if (empty($project['jobs_enabled'])) {
+ $this->io->error('CI/CD is not enabled for this application in code studio. Enable it first and then try again.');
+ return self::FAILURE;
+ }
- // If CI/CD is not enabled for the project in Code Studio.
- if (empty($project['jobs_enabled'])) {
- $this->io->error('CI/CD is not enabled for this application in code studio. Enable it first and then try again.');
- return self::FAILURE;
- }
+ try {
+ $phpVersionAlreadySet = false;
+ // Get all variables of the project.
+ $allProjectVariables = $this->gitLabClient->projects()->variables($project['id']);
+ if (!empty($allProjectVariables)) {
+ $variables = array_column($allProjectVariables, 'value', 'key');
+ $phpVersionAlreadySet = $variables['PHP_VERSION'] ?? false;
+ }
+ // If PHP version is not set in variables.
+ if (!$phpVersionAlreadySet) {
+ $this->gitLabClient->projects()->addVariable($project['id'], 'PHP_VERSION', $phpVersion);
+ } else {
+ // If variable already exists, updating the variable.
+ $this->gitLabClient->projects()->updateVariable($project['id'], 'PHP_VERSION', $phpVersion);
+ }
+ } catch (RuntimeException) {
+ $this->io->error("Unable to update the PHP version to $phpVersion");
+ return self::FAILURE;
+ }
- try {
- $phpVersionAlreadySet = FALSE;
- // Get all variables of the project.
- $allProjectVariables = $this->gitLabClient->projects()->variables($project['id']);
- if (!empty($allProjectVariables)) {
- $variables = array_column($allProjectVariables, 'value', 'key');
- $phpVersionAlreadySet = $variables['PHP_VERSION'] ?? FALSE;
- }
- // If PHP version is not set in variables.
- if (!$phpVersionAlreadySet) {
- $this->gitLabClient->projects()->addVariable($project['id'], 'PHP_VERSION', $phpVersion);
- }
- else {
- // If variable already exists, updating the variable.
- $this->gitLabClient->projects()->updateVariable($project['id'], 'PHP_VERSION', $phpVersion);
- }
- }
- catch (RuntimeException) {
- $this->io->error("Unable to update the PHP version to $phpVersion");
- return self::FAILURE;
+ $this->io->success("PHP version is updated to $phpVersion successfully!");
+ return self::SUCCESS;
}
-
- $this->io->success("PHP version is updated to $phpVersion successfully!");
- return self::SUCCESS;
- }
-
}
diff --git a/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php b/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php
index ec0aa0029..46994b7c1 100644
--- a/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php
+++ b/src/Command/CodeStudio/CodeStudioPipelinesMigrateCommand.php
@@ -1,6 +1,6 @@
addOption('key', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use')
- ->addOption('secret', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use')
- ->addOption('gitlab-token', NULL, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance')
- ->addOption('gitlab-project-id', NULL, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.');
- $this->acceptApplicationUuid();
- $this->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->authenticateWithGitLab();
- $this->writeApiTokenMessage($input);
- $cloudKey = $this->determineApiKey();
- $cloudSecret = $this->determineApiSecret();
- // We may already be authenticated with Acquia Cloud Platform via a refresh token.
- // But, we specifically need an API Token key-pair of Code Studio.
- // So we reauthenticate to be sure we're using the provided credentials.
- $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
- $cloudApplicationUuid = $this->determineCloudApplication();
+ protected function configure(): void
+ {
+ $this
+ ->addOption('key', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use')
+ ->addOption('secret', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use')
+ ->addOption('gitlab-token', null, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance')
+ ->addOption('gitlab-project-id', null, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.');
+ $this->acceptApplicationUuid();
+ $this->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
+ }
- // Get Cloud account.
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $accountAdapter = new Account($acquiaCloudClient);
- $account = $accountAdapter->get();
- $this->setGitLabProjectDescription($cloudApplicationUuid);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->authenticateWithGitLab();
+ $this->writeApiTokenMessage($input);
+ $cloudKey = $this->determineApiKey();
+ $cloudSecret = $this->determineApiSecret();
+ // We may already be authenticated with Acquia Cloud Platform via a refresh token.
+ // But, we specifically need an API Token key-pair of Code Studio.
+ // So we reauthenticate to be sure we're using the provided credentials.
+ $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
+ $cloudApplicationUuid = $this->determineCloudApplication();
- // Get Cloud application.
- $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
- $project = $this->determineGitLabProject($cloudApplication);
+ // Get Cloud account.
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $accountAdapter = new Account($acquiaCloudClient);
+ $account = $accountAdapter->get();
+ $this->setGitLabProjectDescription($cloudApplicationUuid);
- // Migrate acquia-pipeline file.
- $this->checkGitLabCiCdVariables($project);
- $this->validateCwdIsValidDrupalProject();
- $acquiaPipelinesFileDetails = $this->getAcquiaPipelinesFileContents($project);
- $acquiaPipelinesFileContents = $acquiaPipelinesFileDetails['file_contents'];
- $acquiaPipelinesFileName = $acquiaPipelinesFileDetails['filename'];
- $gitlabCiFileContents = $this->getGitLabCiFileTemplate();
- $this->migrateVariablesSection($acquiaPipelinesFileContents, $gitlabCiFileContents);
- $this->migrateEventsSection($acquiaPipelinesFileContents, $gitlabCiFileContents);
- $this->removeEmptyScript($gitlabCiFileContents);
- $this->createGitLabCiFile($gitlabCiFileContents, $acquiaPipelinesFileName);
- $this->io->success([
- "",
- "Migration completed successfully.",
- "Created .gitlab-ci.yml and removed acquia-pipeline.yml file.",
- "In order to run Pipeline, push .gitlab-ci.yaml to Main branch of Code Studio project.",
- "Check your pipeline is running in Code Studio for your project.",
- ]);
+ // Get Cloud application.
+ $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
+ $project = $this->determineGitLabProject($cloudApplication);
- return Command::SUCCESS;
- }
+ // Migrate acquia-pipeline file.
+ $this->checkGitLabCiCdVariables($project);
+ $this->validateCwdIsValidDrupalProject();
+ $acquiaPipelinesFileDetails = $this->getAcquiaPipelinesFileContents($project);
+ $acquiaPipelinesFileContents = $acquiaPipelinesFileDetails['file_contents'];
+ $acquiaPipelinesFileName = $acquiaPipelinesFileDetails['filename'];
+ $gitlabCiFileContents = $this->getGitLabCiFileTemplate();
+ $this->migrateVariablesSection($acquiaPipelinesFileContents, $gitlabCiFileContents);
+ $this->migrateEventsSection($acquiaPipelinesFileContents, $gitlabCiFileContents);
+ $this->removeEmptyScript($gitlabCiFileContents);
+ $this->createGitLabCiFile($gitlabCiFileContents, $acquiaPipelinesFileName);
+ $this->io->success([
+ "",
+ "Migration completed successfully.",
+ "Created .gitlab-ci.yml and removed acquia-pipeline.yml file.",
+ "In order to run Pipeline, push .gitlab-ci.yaml to Main branch of Code Studio project.",
+ "Check your pipeline is running in Code Studio for your project.",
+ ]);
- /**
- * Check whether wizard command is executed by checking the env variable of codestudio project.
- */
- private function checkGitLabCiCdVariables(array $project): void {
- $gitlabCicdVariables = CodeStudioCiCdVariables::getList();
- $gitlabCicdExistingVariables = $this->gitLabClient->projects()->variables($project['id']);
- $existingKeys = array_column($gitlabCicdExistingVariables, 'key');
- foreach ($gitlabCicdVariables as $gitlabCicdVariable) {
- if (!in_array($gitlabCicdVariable, $existingKeys, TRUE)) {
- throw new AcquiaCliException("Code Studio CI/CD variable {$gitlabCicdVariable} is not configured properly");
- }
+ return Command::SUCCESS;
}
- }
- /**
- * Check acquia-pipeline.yml file exists in the root repo and remove ci_config_path from codestudio project.
- *
- * @return array
- */
- private function getAcquiaPipelinesFileContents(array $project): array {
- $pipelinesFilepathYml = Path::join($this->projectDir, 'acquia-pipelines.yml');
- $pipelinesFilepathYaml = Path::join($this->projectDir, 'acquia-pipelines.yaml');
- if ($this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYml) ||
- $this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYaml)
- ) {
- $this->gitLabClient->projects()->update($project['id'], ['ci_config_path' => '']);
- $pipelinesFilenames = ['acquia-pipelines.yml', 'acquia-pipelines.yaml'];
- foreach ($pipelinesFilenames as $pipelinesFilename) {
- $pipelinesFilepath = Path::join($this->projectDir, $pipelinesFilename);
- if (file_exists($pipelinesFilepath)) {
- $fileContents = file_get_contents($pipelinesFilepath);
- return [
- 'filename' => $pipelinesFilename,
- 'file_contents' => Yaml::parse($fileContents, Yaml::PARSE_OBJECT),
-];
+ /**
+ * Check whether wizard command is executed by checking the env variable of codestudio project.
+ */
+ private function checkGitLabCiCdVariables(array $project): void
+ {
+ $gitlabCicdVariables = CodeStudioCiCdVariables::getList();
+ $gitlabCicdExistingVariables = $this->gitLabClient->projects()->variables($project['id']);
+ $existingKeys = array_column($gitlabCicdExistingVariables, 'key');
+ foreach ($gitlabCicdVariables as $gitlabCicdVariable) {
+ if (!in_array($gitlabCicdVariable, $existingKeys, true)) {
+ throw new AcquiaCliException("Code Studio CI/CD variable {$gitlabCicdVariable} is not configured properly");
+ }
}
- }
}
- throw new AcquiaCliException("Missing 'acquia-pipelines.yml' file which is required to migrate the project to Code Studio.");
- }
-
- /**
- * Migrating standard template to .gitlab-ci.yml file.
- *
- * @return array
- */
- private function getGitLabCiFileTemplate(): array {
- return [
- 'include' => ['project' => 'acquia/standard-template', 'file' => '/gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml'],
- ];
- }
+ /**
+ * Check acquia-pipeline.yml file exists in the root repo and remove ci_config_path from codestudio project.
+ *
+ * @return array
+ */
+ private function getAcquiaPipelinesFileContents(array $project): array
+ {
+ $pipelinesFilepathYml = Path::join($this->projectDir, 'acquia-pipelines.yml');
+ $pipelinesFilepathYaml = Path::join($this->projectDir, 'acquia-pipelines.yaml');
+ if (
+ $this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYml) ||
+ $this->localMachineHelper->getFilesystem()->exists($pipelinesFilepathYaml)
+ ) {
+ $this->gitLabClient->projects()->update($project['id'], ['ci_config_path' => '']);
+ $pipelinesFilenames = ['acquia-pipelines.yml', 'acquia-pipelines.yaml'];
+ foreach ($pipelinesFilenames as $pipelinesFilename) {
+ $pipelinesFilepath = Path::join($this->projectDir, $pipelinesFilename);
+ if (file_exists($pipelinesFilepath)) {
+ $fileContents = file_get_contents($pipelinesFilepath);
+ return [
+ 'filename' => $pipelinesFilename,
+ 'file_contents' => Yaml::parse($fileContents, Yaml::PARSE_OBJECT),
+ ];
+ }
+ }
+ }
- /**
- * Migrating `variables` section to .gitlab-ci.yml file.
- */
- private function migrateVariablesSection(mixed $acquiaPipelinesFileContents, mixed &$gitlabCiFileContents): void {
- if (array_key_exists('variables', $acquiaPipelinesFileContents)) {
- $variablesDump = Yaml::dump(['variables' => $acquiaPipelinesFileContents['variables']]);
- $removeGlobal = preg_replace('/global:/', '', $variablesDump);
- $variablesParse = Yaml::parse($removeGlobal);
- $gitlabCiFileContents = array_merge($gitlabCiFileContents, $variablesParse);
- $this->io->success([
- "Migrated `variables` section of acquia-pipelines.yml to .gitlab-ci.yml",
- ]);
- }
- else {
- $this->io->info([
- "Checked acquia-pipeline.yml file for `variables` section",
- ]);
+ throw new AcquiaCliException("Missing 'acquia-pipelines.yml' file which is required to migrate the project to Code Studio.");
}
- }
- private function getPipelinesSection(array $acquiaPipelinesFileContents, string $eventName): mixed {
- if (!array_key_exists('events', $acquiaPipelinesFileContents)) {
- return NULL;
+ /**
+ * Migrating standard template to .gitlab-ci.yml file.
+ *
+ * @return array
+ */
+ private function getGitLabCiFileTemplate(): array
+ {
+ return [
+ 'include' => ['project' => 'acquia/standard-template', 'file' => '/gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml'],
+ ];
}
- if (array_key_exists('build', $acquiaPipelinesFileContents['events']) && empty($acquiaPipelinesFileContents['events']['build'])) {
- return NULL;
+
+ /**
+ * Migrating `variables` section to .gitlab-ci.yml file.
+ */
+ private function migrateVariablesSection(mixed $acquiaPipelinesFileContents, mixed &$gitlabCiFileContents): void
+ {
+ if (array_key_exists('variables', $acquiaPipelinesFileContents)) {
+ $variablesDump = Yaml::dump(['variables' => $acquiaPipelinesFileContents['variables']]);
+ $removeGlobal = preg_replace('/global:/', '', $variablesDump);
+ $variablesParse = Yaml::parse($removeGlobal);
+ $gitlabCiFileContents = array_merge($gitlabCiFileContents, $variablesParse);
+ $this->io->success([
+ "Migrated `variables` section of acquia-pipelines.yml to .gitlab-ci.yml",
+ ]);
+ } else {
+ $this->io->info([
+ "Checked acquia-pipeline.yml file for `variables` section",
+ ]);
+ }
}
- if (!array_key_exists($eventName, $acquiaPipelinesFileContents['events'])) {
- return NULL;
+
+ private function getPipelinesSection(array $acquiaPipelinesFileContents, string $eventName): mixed
+ {
+ if (!array_key_exists('events', $acquiaPipelinesFileContents)) {
+ return null;
+ }
+ if (array_key_exists('build', $acquiaPipelinesFileContents['events']) && empty($acquiaPipelinesFileContents['events']['build'])) {
+ return null;
+ }
+ if (!array_key_exists($eventName, $acquiaPipelinesFileContents['events'])) {
+ return null;
+ }
+ return $acquiaPipelinesFileContents['events'][$eventName]['steps'] ?? null;
}
- return $acquiaPipelinesFileContents['events'][$eventName]['steps'] ?? NULL;
- }
- private function migrateEventsSection(array $acquiaPipelinesFileContents, array &$gitlabCiFileContents): void {
- // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
- $eventsMap = [
- 'build' => [
+ private function migrateEventsSection(array $acquiaPipelinesFileContents, array &$gitlabCiFileContents): void
+ {
+ // phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys
+ $eventsMap = [
+ 'build' => [
'skip' => [
- 'composer install' => [
- 'message' => 'Code Studio AutoDevOps will run `composer install` by default. Skipping migration of this command in your acquia-pipelines.yml file:',
- 'prompt' => FALSE,
- ],
- '${BLT_DIR}' => [
- 'message' => 'Code Studio AutoDevOps will run BLT commands for you by default. Do you want to migrate the following command?',
- 'prompt' => TRUE,
- ],
+ 'composer install' => [
+ 'message' => 'Code Studio AutoDevOps will run `composer install` by default. Skipping migration of this command in your acquia-pipelines.yml file:',
+ 'prompt' => false,
+ ],
+ '${BLT_DIR}' => [
+ 'message' => 'Code Studio AutoDevOps will run BLT commands for you by default. Do you want to migrate the following command?',
+ 'prompt' => true,
+ ],
],
'default_stage' => 'Test Drupal',
'stage' => [
- 'setup' => 'Build Drupal',
- 'npm run build' => 'Build Drupal',
- 'validate' => 'Test Drupal',
- 'tests' => 'Test Drupal',
- 'test' => 'Test Drupal',
- 'npm test' => 'Test Drupal',
- 'artifact' => 'Deploy Drupal',
- 'deploy' => 'Deploy Drupal',
+ 'setup' => 'Build Drupal',
+ 'npm run build' => 'Build Drupal',
+ 'validate' => 'Test Drupal',
+ 'tests' => 'Test Drupal',
+ 'test' => 'Test Drupal',
+ 'npm test' => 'Test Drupal',
+ 'artifact' => 'Deploy Drupal',
+ 'deploy' => 'Deploy Drupal',
],
'needs' => [
- 'Build Code',
- 'Manage Secrets',
+ 'Build Code',
+ 'Manage Secrets',
+ ],
],
- ],
- 'post-deploy' => [
+ 'post-deploy' => [
'skip' => [
- 'launch_ode' => [
- 'message' => 'Code Studio AutoDevOps will run Launch a new Continuous Delivery Environment (CDE) automatically for new merge requests. Skipping migration of this command in your acquia-pipelines.yml file:',
- 'prompt' => FALSE,
- ],
+ 'launch_ode' => [
+ 'message' => 'Code Studio AutoDevOps will run Launch a new Continuous Delivery Environment (CDE) automatically for new merge requests. Skipping migration of this command in your acquia-pipelines.yml file:',
+ 'prompt' => false,
+ ],
],
'default_stage' => 'Deploy Drupal',
'stage' => [
- 'launch_ode' => 'Deploy Drupal',
+ 'launch_ode' => 'Deploy Drupal',
],
'needs' => [
- 'Create artifact from branch',
+ 'Create artifact from branch',
+ ],
],
- ],
- ];
- // phpcs:enable
+ ];
+ // phpcs:enable
- $codeStudioJobs = [];
- foreach ($eventsMap as $eventName => $eventMap) {
- $eventSteps = $this->getPipelinesSection($acquiaPipelinesFileContents, $eventName);
- if ($eventSteps) {
- foreach ($eventSteps as $step) {
- $scriptName = array_keys($step)[0];
- if (!array_key_exists('script', $step[$scriptName]) || empty($step[$scriptName]['script'])) {
- continue;
- }
- if ($stage = $this->assignStageFromKeywords($eventMap['stage'], $scriptName)) {
- $codeStudioJobs[$scriptName]['stage'] = $stage;
- }
- foreach ($step[$scriptName]['script'] as $command) {
- foreach ($eventMap['skip'] as $needle => $messageConfig) {
- if (str_contains($command, $needle)) {
- if ($messageConfig['prompt']) {
- $answer = $this->io->confirm($messageConfig['message'] . PHP_EOL . $command, FALSE);
- if ($answer == 1) {
- $codeStudioJobs[$scriptName]['script'][] = $command;
- $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script']));
- }
- else if (($key = array_search($command, $codeStudioJobs[$scriptName]['script'], TRUE)) !== FALSE) {
- unset($codeStudioJobs[$scriptName]['script'][$key]);
- }
- }
- else {
- $this->io->note([
- $messageConfig['message'],
- $command,
- ]);
- }
- break;
- }
+ $codeStudioJobs = [];
+ foreach ($eventsMap as $eventName => $eventMap) {
+ $eventSteps = $this->getPipelinesSection($acquiaPipelinesFileContents, $eventName);
+ if ($eventSteps) {
+ foreach ($eventSteps as $step) {
+ $scriptName = array_keys($step)[0];
+ if (!array_key_exists('script', $step[$scriptName]) || empty($step[$scriptName]['script'])) {
+ continue;
+ }
+ if ($stage = $this->assignStageFromKeywords($eventMap['stage'], $scriptName)) {
+ $codeStudioJobs[$scriptName]['stage'] = $stage;
+ }
+ foreach ($step[$scriptName]['script'] as $command) {
+ foreach ($eventMap['skip'] as $needle => $messageConfig) {
+ if (str_contains($command, $needle)) {
+ if ($messageConfig['prompt']) {
+ $answer = $this->io->confirm($messageConfig['message'] . PHP_EOL . $command, false);
+ if ($answer == 1) {
+ $codeStudioJobs[$scriptName]['script'][] = $command;
+ $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script']));
+ } elseif (($key = array_search($command, $codeStudioJobs[$scriptName]['script'], true)) !== false) {
+ unset($codeStudioJobs[$scriptName]['script'][$key]);
+ }
+ } else {
+ $this->io->note([
+ $messageConfig['message'],
+ $command,
+ ]);
+ }
+ break;
+ }
- if (array_key_exists($scriptName, $codeStudioJobs) && array_key_exists('script', $codeStudioJobs[$scriptName]) && in_array($command, $codeStudioJobs[$scriptName]['script'], TRUE)) {
- break;
- }
- if (!array_key_exists($scriptName, $eventMap['skip']) ) {
- $codeStudioJobs[$scriptName]['script'][] = $command;
- $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script']));
- }
- else if ($scriptName === 'launch_ode') {
- $codeStudioJobs[$scriptName]['script'][] = $command;
- }
- }
- if (array_key_exists($scriptName, $codeStudioJobs) && !array_key_exists('stage', $codeStudioJobs[$scriptName])
- && $stage = $this->assignStageFromKeywords($eventMap['stage'], $command)) {
- $codeStudioJobs[$scriptName]['stage'] = $stage;
+ if (array_key_exists($scriptName, $codeStudioJobs) && array_key_exists('script', $codeStudioJobs[$scriptName]) && in_array($command, $codeStudioJobs[$scriptName]['script'], true)) {
+ break;
+ }
+ if (!array_key_exists($scriptName, $eventMap['skip'])) {
+ $codeStudioJobs[$scriptName]['script'][] = $command;
+ $codeStudioJobs[$scriptName]['script'] = array_values(array_unique($codeStudioJobs[$scriptName]['script']));
+ } elseif ($scriptName === 'launch_ode') {
+ $codeStudioJobs[$scriptName]['script'][] = $command;
+ }
+ }
+ if (
+ array_key_exists($scriptName, $codeStudioJobs) && !array_key_exists('stage', $codeStudioJobs[$scriptName])
+ && $stage = $this->assignStageFromKeywords($eventMap['stage'], $command)
+ ) {
+ $codeStudioJobs[$scriptName]['stage'] = $stage;
+ }
+ }
+ if (!array_key_exists('stage', $codeStudioJobs[$scriptName])) {
+ $codeStudioJobs[$scriptName]['stage'] = $eventMap['default_stage'];
+ }
+ $codeStudioJobs[$scriptName]['needs'] = $eventMap['needs'];
+ }
+ $gitlabCiFileContents = array_merge($gitlabCiFileContents, $codeStudioJobs);
+ $this->io->success([
+ "Completed migration of the $eventName step in your acquia-pipelines.yml file",
+ ]);
+ } else {
+ $this->io->writeln([
+ "acquia-pipeline.yml file does not contain $eventName step to migrate",
+ ]);
}
- }
- if (!array_key_exists('stage', $codeStudioJobs[$scriptName])) {
- $codeStudioJobs[$scriptName]['stage'] = $eventMap['default_stage'];
- }
- $codeStudioJobs[$scriptName]['needs'] = $eventMap['needs'];
}
- $gitlabCiFileContents = array_merge($gitlabCiFileContents, $codeStudioJobs);
- $this->io->success([
- "Completed migration of the $eventName step in your acquia-pipelines.yml file",
- ]);
- }
- else {
- $this->io->writeln([
- "acquia-pipeline.yml file does not contain $eventName step to migrate",
- ]);
- }
}
- }
-
- /**
- * Removing empty script.
- */
- private function removeEmptyScript(array &$gitlabCiFileContents): void {
- foreach ($gitlabCiFileContents as $key => $value) {
- if (array_key_exists('script', $value) && empty($value['script'])) {
- unset($gitlabCiFileContents[$key]);
- }
+ /**
+ * Removing empty script.
+ */
+ private function removeEmptyScript(array &$gitlabCiFileContents): void
+ {
+ foreach ($gitlabCiFileContents as $key => $value) {
+ if (array_key_exists('script', $value) && empty($value['script'])) {
+ unset($gitlabCiFileContents[$key]);
+ }
+ }
}
- }
- /**
- * Creating .gitlab-ci.yml file.
- */
- private function createGitLabCiFile(array $contents, string|iterable $acquiaPipelinesFileName): void {
- $gitlabCiFilepath = Path::join($this->projectDir, '.gitlab-ci.yml');
- $this->localMachineHelper->getFilesystem()->dumpFile($gitlabCiFilepath, Yaml::dump($contents, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
- $this->localMachineHelper->getFilesystem()->remove($acquiaPipelinesFileName);
- }
-
- private function assignStageFromKeywords(array $keywords, string $haystack): ?string {
- foreach ($keywords as $needle => $stage) {
- if (str_contains($haystack, $needle)) {
- return $stage;
- }
+ /**
+ * Creating .gitlab-ci.yml file.
+ */
+ private function createGitLabCiFile(array $contents, string|iterable $acquiaPipelinesFileName): void
+ {
+ $gitlabCiFilepath = Path::join($this->projectDir, '.gitlab-ci.yml');
+ $this->localMachineHelper->getFilesystem()->dumpFile($gitlabCiFilepath, Yaml::dump($contents, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
+ $this->localMachineHelper->getFilesystem()->remove($acquiaPipelinesFileName);
}
- return NULL;
- }
+ private function assignStageFromKeywords(array $keywords, string $haystack): ?string
+ {
+ foreach ($keywords as $needle => $stage) {
+ if (str_contains($haystack, $needle)) {
+ return $stage;
+ }
+ }
+ return null;
+ }
}
diff --git a/src/Command/CodeStudio/CodeStudioWizardCommand.php b/src/Command/CodeStudio/CodeStudioWizardCommand.php
index afeea6b30..1e1c4ded8 100644
--- a/src/Command/CodeStudio/CodeStudioWizardCommand.php
+++ b/src/Command/CodeStudio/CodeStudioWizardCommand.php
@@ -1,6 +1,6 @@
addOption('key', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use')
+ ->addOption('secret', null, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use')
+ ->addOption('gitlab-token', null, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance')
+ ->addOption('gitlab-project-id', null, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.')
+ ->addOption('gitlab-host-name', null, InputOption::VALUE_REQUIRED, 'The GitLab hostname.');
+ $this->acceptApplicationUuid();
+ }
- protected function configure(): void {
- $this
- ->addOption('key', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API token that Code Studio will use')
- ->addOption('secret', NULL, InputOption::VALUE_REQUIRED, 'The Cloud Platform API secret that Code Studio will use')
- ->addOption('gitlab-token', NULL, InputOption::VALUE_REQUIRED, 'The GitLab personal access token that will be used to communicate with the GitLab instance')
- ->addOption('gitlab-project-id', NULL, InputOption::VALUE_REQUIRED, 'The project ID (an integer) of the GitLab project to configure.')
- ->addOption('gitlab-host-name', NULL, InputOption::VALUE_REQUIRED, 'The GitLab hostname.');
- $this->acceptApplicationUuid();
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->checklist = new Checklist($output);
+ $this->authenticateWithGitLab();
+ $this->writeApiTokenMessage($input);
+ $cloudKey = $this->determineApiKey();
+ $cloudSecret = $this->determineApiSecret();
+ // We may already be authenticated with Acquia Cloud Platform via a refresh token.
+ // But, we specifically need an API Token key-pair of Code Studio.
+ // So we reauthenticate to be sure we're using the provided credentials.
+ $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
+ $phpVersion = null;
+ $nodeVersion = null;
+ $projectType = $this->getListOfProjectType();
+ $projectSelected = $this->io->choice('Select a project type', $projectType, "Drupal_project");
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->checklist = new Checklist($output);
- $this->authenticateWithGitLab();
- $this->writeApiTokenMessage($input);
- $cloudKey = $this->determineApiKey();
- $cloudSecret = $this->determineApiSecret();
- // We may already be authenticated with Acquia Cloud Platform via a refresh token.
- // But, we specifically need an API Token key-pair of Code Studio.
- // So we reauthenticate to be sure we're using the provided credentials.
- $this->reAuthenticate($cloudKey, $cloudSecret, $this->cloudCredentials->getBaseUri(), $this->cloudCredentials->getAccountsUri());
- $phpVersion = NULL;
- $nodeVersion = NULL;
- $projectType = $this->getListOfProjectType();
- $projectSelected = $this->io->choice('Select a project type', $projectType, "Drupal_project");
+ switch ($projectSelected) {
+ case "Drupal_project":
+ $phpVersions = [
+ 'PHP_version_8.1' => "8.1",
+ 'PHP_version_8.2' => "8.2",
+ 'PHP_version_8.3' => "8.3",
+ ];
+ $project = $this->io->choice('Select a PHP version', array_values($phpVersions), "8.1");
+ $project = array_search($project, $phpVersions, true);
+ $phpVersion = $phpVersions[$project];
+ break;
+ case "Node_project":
+ $nodeVersions = [
+ 'NODE_version_18.17.1' => "18.17.1",
+ 'NODE_version_20.5.1' => "20.5.1",
+ ];
+ $project = $this->io->choice('Select a NODE version', array_values($nodeVersions), "18.17.1");
+ $project = array_search($project, $nodeVersions, true);
+ $nodeVersion = $nodeVersions[$project];
+ break;
+ }
- switch ($projectSelected) {
- case "Drupal_project":
- $phpVersions = [
- 'PHP_version_8.1' => "8.1",
- 'PHP_version_8.2' => "8.2",
- 'PHP_version_8.3' => "8.3",
- ];
- $project = $this->io->choice('Select a PHP version', array_values($phpVersions), "8.1");
- $project = array_search($project, $phpVersions, TRUE);
- $phpVersion = $phpVersions[$project];
- break;
- case "Node_project":
- $nodeVersions = [
- 'NODE_version_18.17.1' => "18.17.1",
- 'NODE_version_20.5.1' => "20.5.1",
- ];
- $project = $this->io->choice('Select a NODE version', array_values($nodeVersions), "18.17.1");
- $project = array_search($project, $nodeVersions, TRUE);
- $nodeVersion = $nodeVersions[$project];
- break;
- }
+ $appUuid = $this->determineCloudApplication();
- $appUuid = $this->determineCloudApplication();
+ // Get Cloud account.
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $accountAdapter = new Account($acquiaCloudClient);
+ $account = $accountAdapter->get();
+ $this->validateRequiredCloudPermissions(
+ $acquiaCloudClient,
+ $appUuid,
+ $account,
+ [
+ "deploy to non-prod",
+ // Add SSH key to git repository.
+ "add ssh key to git",
+ // Add SSH key to non-production environments.
+ "add ssh key to non-prod",
+ // Add a CD environment.
+ "add an environment",
+ // Delete a CD environment.
+ "delete an environment",
+ // Manage environment variables on a non-production environment.
+ "administer environment variables on non-prod",
+ ]
+ );
+ $this->setGitLabProjectDescription($appUuid);
- // Get Cloud account.
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $accountAdapter = new Account($acquiaCloudClient);
- $account = $accountAdapter->get();
- $this->validateRequiredCloudPermissions(
- $acquiaCloudClient,
- $appUuid,
- $account,
- [
- "deploy to non-prod",
- // Add SSH key to git repository.
- "add ssh key to git",
- // Add SSH key to non-production environments.
- "add ssh key to non-prod",
- // Add a CD environment.
- "add an environment",
- // Delete a CD environment.
- "delete an environment",
- // Manage environment variables on a non-production environment.
- "administer environment variables on non-prod",
- ]
- );
- $this->setGitLabProjectDescription($appUuid);
+ // Get Cloud application.
+ $cloudApplication = $this->getCloudApplication($appUuid);
+ $project = $this->determineGitLabProject($cloudApplication);
- // Get Cloud application.
- $cloudApplication = $this->getCloudApplication($appUuid);
- $project = $this->determineGitLabProject($cloudApplication);
+ $this->io->writeln([
+ "",
+ "This command will configure the Code Studio project {$project['path_with_namespace']} for automatic deployment to the",
+ "Acquia Cloud Platform application {$cloudApplication->name} ($appUuid)",
+ "using credentials (API Token and SSH Key) belonging to {$account->mail}.",
+ "",
+ "If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured.",
+ ]);
+ $answer = $this->io->confirm('Do you want to continue?');
+ if (!$answer) {
+ return Command::SUCCESS;
+ }
- $this->io->writeln([
- "",
- "This command will configure the Code Studio project {$project['path_with_namespace']} for automatic deployment to the",
- "Acquia Cloud Platform application {$cloudApplication->name} ($appUuid)",
- "using credentials (API Token and SSH Key) belonging to {$account->mail}.",
- "",
- "If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured.",
- ]);
- $answer = $this->io->confirm('Do you want to continue?');
- if (!$answer) {
- return Command::SUCCESS;
- }
+ $projectAccessTokenName = 'acquia-codestudio';
+ $projectAccessToken = $this->createProjectAccessToken($project, $projectAccessTokenName);
+ $this->updateGitLabProject($project);
+ switch ($projectSelected) {
+ case "Drupal_project":
+ $this->setGitLabCiCdVariablesForPhpProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion);
+ $this->createScheduledPipeline($project);
+ break;
+ case "Node_project":
+ $parameters = [
+ 'ci_config_path' => 'gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml@acquia/node-template',
+ ];
+ $client = $this->getGitLabClient();
+ $client->projects()->update($project['id'], $parameters);
+ $this->setGitLabCiCdVariablesForNodeProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion);
+ break;
+ }
- $projectAccessTokenName = 'acquia-codestudio';
- $projectAccessToken = $this->createProjectAccessToken($project, $projectAccessTokenName);
- $this->updateGitLabProject($project);
- switch ($projectSelected) {
- case "Drupal_project":
- $this->setGitLabCiCdVariablesForPhpProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion);
- $this->createScheduledPipeline($project);
- break;
- case "Node_project":
- $parameters = [
- 'ci_config_path' => 'gitlab-ci/Auto-DevOps.acquia.gitlab-ci.yml@acquia/node-template',
- ];
- $client = $this->getGitLabClient();
- $client->projects()->update($project['id'], $parameters);
- $this->setGitLabCiCdVariablesForNodeProject($project, $appUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion);
- break;
- }
-
- $this->io->success([
- "Successfully configured the Code Studio project!",
- "This project will now use Acquia's Drupal optimized AutoDevOps to build, test, and deploy your code automatically to Acquia Cloud Platform via CI/CD pipelines.",
- "You can visit it here:",
- $project['web_url'],
- "",
- "Next, you should use git to push code to your Code Studio project. E.g.,",
- " git remote add codestudio {$project['http_url_to_repo']}",
- " git push codestudio",
- ]);
- $this->io->note(["If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured."]);
+ $this->io->success([
+ "Successfully configured the Code Studio project!",
+ "This project will now use Acquia's Drupal optimized AutoDevOps to build, test, and deploy your code automatically to Acquia Cloud Platform via CI/CD pipelines.",
+ "You can visit it here:",
+ $project['web_url'],
+ "",
+ "Next, you should use git to push code to your Code Studio project. E.g.,",
+ " git remote add codestudio {$project['http_url_to_repo']}",
+ " git push codestudio",
+ ]);
+ $this->io->note(["If the {$account->mail} Cloud account is deleted in the future, this Code Studio project will need to be re-configured."]);
- return Command::SUCCESS;
- }
+ return Command::SUCCESS;
+ }
- /**
- * @return array|null
- */
- private function getGitLabScheduleByDescription(array $project, string $scheduledPipelineDescription): ?array {
- $existingSchedules = $this->gitLabClient->schedules()->showAll($project['id']);
- foreach ($existingSchedules as $schedule) {
- if ($schedule['description'] == $scheduledPipelineDescription) {
- return $schedule;
- }
+ /**
+ * @return array|null
+ */
+ private function getGitLabScheduleByDescription(array $project, string $scheduledPipelineDescription): ?array
+ {
+ $existingSchedules = $this->gitLabClient->schedules()->showAll($project['id']);
+ foreach ($existingSchedules as $schedule) {
+ if ($schedule['description'] == $scheduledPipelineDescription) {
+ return $schedule;
+ }
+ }
+ return null;
}
- return NULL;
- }
- /**
- * @return array|null ?
- */
- private function getGitLabProjectAccessTokenByName(array $project, string $name): ?array {
- $existingProjectAccessTokens = $this->gitLabClient->projects()->projectAccessTokens($project['id']);
- foreach ($existingProjectAccessTokens as $key => $token) {
- if ($token['name'] == $name) {
- return $token;
- }
+ /**
+ * @return array|null ?
+ */
+ private function getGitLabProjectAccessTokenByName(array $project, string $name): ?array
+ {
+ $existingProjectAccessTokens = $this->gitLabClient->projects()->projectAccessTokens($project['id']);
+ foreach ($existingProjectAccessTokens as $key => $token) {
+ if ($token['name'] == $name) {
+ return $token;
+ }
+ }
+ return null;
}
- return NULL;
- }
- /**
- * @return array|null ?
- */
- private function getListOfProjectType(): ?array {
- $array = [
- 'Drupal_project',
- 'Node_project',
- ];
- return $array;
- }
+ /**
+ * @return array|null ?
+ */
+ private function getListOfProjectType(): ?array
+ {
+ $array = [
+ 'Drupal_project',
+ 'Node_project',
+ ];
+ return $array;
+ }
- private function createProjectAccessToken(array $project, string $projectAccessTokenName): string {
- $this->io->writeln("Creating project access token...");
+ private function createProjectAccessToken(array $project, string $projectAccessTokenName): string
+ {
+ $this->io->writeln("Creating project access token...");
- if ($existingToken = $this->getGitLabProjectAccessTokenByName($project, $projectAccessTokenName)) {
- $this->checklist->addItem("Deleting access token named $projectAccessTokenName");
- $this->gitLabClient->projects()
+ if ($existingToken = $this->getGitLabProjectAccessTokenByName($project, $projectAccessTokenName)) {
+ $this->checklist->addItem("Deleting access token named $projectAccessTokenName");
+ $this->gitLabClient->projects()
->deleteProjectAccessToken($project['id'], $existingToken['id']);
- $this->checklist->completePreviousItem();
- }
- $this->checklist->addItem("Creating access token named $projectAccessTokenName");
- $projectAccessToken = $this->gitLabClient->projects()
+ $this->checklist->completePreviousItem();
+ }
+ $this->checklist->addItem("Creating access token named $projectAccessTokenName");
+ $projectAccessToken = $this->gitLabClient->projects()
->createProjectAccessToken($project['id'], [
- 'expires_at' => new DateTime('+365 days'),
- 'name' => $projectAccessTokenName,
- 'scopes' => ['api', 'write_repository'],
+ 'expires_at' => new DateTime('+365 days'),
+ 'name' => $projectAccessTokenName,
+ 'scopes' => ['api', 'write_repository'],
]);
- $this->checklist->completePreviousItem();
- return $projectAccessToken['token'];
- }
-
- private function setGitLabCiCdVariablesForPhpProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $phpVersion): void {
- $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}..");
- $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForPhp($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion);
- $gitlabCicdExistingVariables = $this->gitLabClient->projects()
- ->variables($project['id']);
- $gitlabCicdExistingVariablesKeyed = [];
- foreach ($gitlabCicdExistingVariables as $variable) {
- $key = $variable['key'];
- $gitlabCicdExistingVariablesKeyed[$key] = $variable;
+ $this->checklist->completePreviousItem();
+ return $projectAccessToken['token'];
}
- foreach ($gitlabCicdVariables as $variable) {
- $this->checklist->addItem("Setting GitLab CI/CD variables for {$variable['key']}");
- if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) {
- $this->gitLabClient->projects()
- ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
- }
- else {
- $this->gitLabClient->projects()
- ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
- }
- $this->checklist->completePreviousItem();
- }
- }
+ private function setGitLabCiCdVariablesForPhpProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $phpVersion): void
+ {
+ $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}..");
+ $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForPhp($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $phpVersion);
+ $gitlabCicdExistingVariables = $this->gitLabClient->projects()
+ ->variables($project['id']);
+ $gitlabCicdExistingVariablesKeyed = [];
+ foreach ($gitlabCicdExistingVariables as $variable) {
+ $key = $variable['key'];
+ $gitlabCicdExistingVariablesKeyed[$key] = $variable;
+ }
- private function setGitLabCiCdVariablesForNodeProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $nodeVersion): void {
- $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}..");
- $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForNode($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion);
- $gitlabCicdExistingVariables = $this->gitLabClient->projects()
- ->variables($project['id']);
- $gitlabCicdExistingVariablesKeyed = [];
- foreach ($gitlabCicdExistingVariables as $variable) {
- $key = $variable['key'];
- $gitlabCicdExistingVariablesKeyed[$key] = $variable;
+ foreach ($gitlabCicdVariables as $variable) {
+ $this->checklist->addItem("Setting GitLab CI/CD variables for {$variable['key']}");
+ if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) {
+ $this->gitLabClient->projects()
+ ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
+ } else {
+ $this->gitLabClient->projects()
+ ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
+ }
+ $this->checklist->completePreviousItem();
+ }
}
- foreach ($gitlabCicdVariables as $variable) {
- $this->checklist->addItem("Setting CI/CD variable {$variable['key']}");
- if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) {
- $this->gitLabClient->projects()
- ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
- }
- else {
- $this->gitLabClient->projects()
- ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], NULL, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
- }
- $this->checklist->completePreviousItem();
+ private function setGitLabCiCdVariablesForNodeProject(array $project, string $cloudApplicationUuid, string $cloudKey, string $cloudSecret, string $projectAccessTokenName, string $projectAccessToken, string $nodeVersion): void
+ {
+ $this->io->writeln("Setting GitLab CI/CD variables for {$project['path_with_namespace']}..");
+ $gitlabCicdVariables = CodeStudioCiCdVariables::getDefaultsForNode($cloudApplicationUuid, $cloudKey, $cloudSecret, $projectAccessTokenName, $projectAccessToken, $nodeVersion);
+ $gitlabCicdExistingVariables = $this->gitLabClient->projects()
+ ->variables($project['id']);
+ $gitlabCicdExistingVariablesKeyed = [];
+ foreach ($gitlabCicdExistingVariables as $variable) {
+ $key = $variable['key'];
+ $gitlabCicdExistingVariablesKeyed[$key] = $variable;
+ }
+
+ foreach ($gitlabCicdVariables as $variable) {
+ $this->checklist->addItem("Setting CI/CD variable {$variable['key']}");
+ if (!array_key_exists($variable['key'], $gitlabCicdExistingVariablesKeyed)) {
+ $this->gitLabClient->projects()
+ ->addVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
+ } else {
+ $this->gitLabClient->projects()
+ ->updateVariable($project['id'], $variable['key'], $variable['value'], $variable['protected'], null, ['masked' => $variable['masked'], 'variable_type' => $variable['variable_type']]);
+ }
+ $this->checklist->completePreviousItem();
+ }
}
- }
- private function createScheduledPipeline(array $project): void {
- $this->io->writeln("Creating scheduled pipeline");
- $scheduledPipelineDescription = "Code Studio Automatic Updates";
+ private function createScheduledPipeline(array $project): void
+ {
+ $this->io->writeln("Creating scheduled pipeline");
+ $scheduledPipelineDescription = "Code Studio Automatic Updates";
- if (!$this->getGitLabScheduleByDescription($project, $scheduledPipelineDescription)) {
- $this->checklist->addItem("Creating scheduled pipeline $scheduledPipelineDescription");
- $pipeline = $this->gitLabClient->schedules()->create($project['id'], [
- // Every Thursday at midnight.
- 'cron' => '0 0 * * 4',
- 'description' => $scheduledPipelineDescription,
- 'ref' => $project['default_branch'],
- ]);
- $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [
- 'key' => 'ACQUIA_JOBS_DEPRECATED_UPDATE',
- 'value' => 'true',
- ]);
- $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [
- 'key' => 'ACQUIA_JOBS_COMPOSER_UPDATE',
- 'value' => 'true',
- ]);
+ if (!$this->getGitLabScheduleByDescription($project, $scheduledPipelineDescription)) {
+ $this->checklist->addItem("Creating scheduled pipeline $scheduledPipelineDescription");
+ $pipeline = $this->gitLabClient->schedules()->create($project['id'], [
+ // Every Thursday at midnight.
+ 'cron' => '0 0 * * 4',
+ 'description' => $scheduledPipelineDescription,
+ 'ref' => $project['default_branch'],
+ ]);
+ $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [
+ 'key' => 'ACQUIA_JOBS_DEPRECATED_UPDATE',
+ 'value' => 'true',
+ ]);
+ $this->gitLabClient->schedules()->addVariable($project['id'], $pipeline['id'], [
+ 'key' => 'ACQUIA_JOBS_COMPOSER_UPDATE',
+ 'value' => 'true',
+ ]);
+ } else {
+ $this->checklist->addItem("Scheduled pipeline named $scheduledPipelineDescription already exists");
+ }
+ $this->checklist->completePreviousItem();
}
- else {
- $this->checklist->addItem("Scheduled pipeline named $scheduledPipelineDescription already exists");
- }
- $this->checklist->completePreviousItem();
- }
- private function updateGitLabProject(array $project): void {
- // Setting the description to match the known pattern will allow us to automatically find the project next time.
- if ($project['description'] !== $this->gitLabProjectDescription) {
- $this->gitLabClient->projects()->update($project['id'], $this->getGitLabProjectDefaults());
- try {
- $this->gitLabClient->projects()->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png');
- }
- catch (ValidationFailedException) {
- $this->io->warning("Failed to upload project avatar");
- }
+ private function updateGitLabProject(array $project): void
+ {
+ // Setting the description to match the known pattern will allow us to automatically find the project next time.
+ if ($project['description'] !== $this->gitLabProjectDescription) {
+ $this->gitLabClient->projects()->update($project['id'], $this->getGitLabProjectDefaults());
+ try {
+ $this->gitLabClient->projects()->uploadAvatar($project['id'], __DIR__ . '/drupal_icon.png');
+ } catch (ValidationFailedException) {
+ $this->io->warning("Failed to upload project avatar");
+ }
+ }
}
- }
-
}
diff --git a/src/Command/CommandBase.php b/src/Command/CommandBase.php
index 1d07953d4..51b7b219d 100644
--- a/src/Command/CommandBase.php
+++ b/src/Command/CommandBase.php
@@ -1,6 +1,6 @@
logger = $logger;
- $this->setLocalDbPassword();
- $this->setLocalDbUser();
- $this->setLocalDbName();
- $this->setLocalDbHost();
- parent::__construct();
- if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class)) {
- $this->appendHelp('This command requires authentication via the Cloud Platform API.');
- }
- if ((new \ReflectionClass(static::class))->getAttributes(RequireDb::class)) {
- $this->appendHelp('This command requires an active database connection. Set the following environment variables prior to running this command: '
- . 'ACLI_DB_HOST, ACLI_DB_NAME, ACLI_DB_USER, ACLI_DB_PASSWORD');
- }
- }
-
- public function appendHelp(string $helpText): void {
- $currentHelp = $this->getHelp();
- $helpText = $currentHelp ? $currentHelp . "\n" . $helpText : $currentHelp . $helpText;
- $this->setHelp($helpText);
- }
-
- protected static function getUuidRegexConstraint(): Regex {
- return new Regex([
- 'message' => 'This is not a valid UUID.',
- 'pattern' => '/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i',
- ]);
- }
-
- public function setProjectDir(string $projectDir): void {
- $this->projectDir = $projectDir;
- }
-
- public function getProjectDir(): string {
- return $this->projectDir;
- }
-
- private function setLocalDbUser(): void {
- if (getenv('ACLI_DB_USER')) {
- $this->localDbUser = getenv('ACLI_DB_USER');
- }
- }
-
- public function getLocalDbUser(): string {
- return $this->localDbUser;
- }
-
- private function setLocalDbPassword(): void {
- if (getenv('ACLI_DB_PASSWORD')) {
- $this->localDbPassword = getenv('ACLI_DB_PASSWORD');
- }
- }
-
- public function getLocalDbPassword(): string {
- return $this->localDbPassword;
- }
-
- private function setLocalDbName(): void {
- if (getenv('ACLI_DB_NAME')) {
- $this->localDbName = getenv('ACLI_DB_NAME');
- }
- }
-
- public function getLocalDbName(): string {
- return $this->localDbName;
- }
-
- private function setLocalDbHost(): void {
- if (getenv('ACLI_DB_HOST')) {
- $this->localDbHost = getenv('ACLI_DB_HOST');
- }
- }
-
- public function getLocalDbHost(): string {
- return $this->localDbHost;
- }
-
- /**
- * Initializes the command just after the input has been validated.
- */
- protected function initialize(InputInterface $input, OutputInterface $output): void {
- $this->input = $input;
- $this->output = $output;
- $this->io = new SymfonyStyle($input, $output);
- // Register custom progress bar format.
- ProgressBar::setFormatDefinition(
- 'message',
- "%current%/%max% [%bar%] %percent:3s%% -- %elapsed:6s%/%estimated:-6s%\n %message%"
- );
- $this->formatter = $this->getHelper('formatter');
-
- $this->output->writeln('Acquia CLI version: ' . $this->getApplication()->getVersion(), OutputInterface::VERBOSITY_DEBUG);
- if (getenv('ACLI_NO_TELEMETRY') !== 'true') {
- $this->checkAndPromptTelemetryPreference();
- $this->telemetryHelper->initialize();
- }
- $this->checkAuthentication();
-
- $this->fillMissingRequiredApplicationUuid($input, $output);
- $this->convertApplicationAliasToUuid($input);
- $this->convertUserAliasToUuid($input, 'userUuid', 'organizationUuid');
- $this->convertEnvironmentAliasToUuid($input, 'environmentId');
- $this->convertEnvironmentAliasToUuid($input, 'source-environment');
- $this->convertEnvironmentAliasToUuid($input, 'destination-environment');
- $this->convertEnvironmentAliasToUuid($input, 'source');
- $this->convertNotificationToUuid($input, 'notificationUuid');
- $this->convertNotificationToUuid($input, 'notification-uuid');
-
- if ($latest = $this->checkForNewVersion()) {
- $this->output->writeln("Acquia CLI $latest is available. Run acli self-update> to update.");
- }
- }
-
- /**
- * Check if telemetry preference is set, prompt if not.
- */
- public function checkAndPromptTelemetryPreference(): void {
- $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY);
- if ($this->getName() !== 'telemetry' && (!isset($sendTelemetry)) && $this->input->isInteractive()) {
- $this->output->writeln('We strive to give you the best tools for development.');
- $this->output->writeln('You can really help us improve by sharing anonymous performance and usage data.');
- $style = new SymfonyStyle($this->input, $this->output);
- $pref = $style->confirm('Would you like to share anonymous performance usage and data?');
- $this->datastoreCloud->set(DataStoreContract::SEND_TELEMETRY, $pref);
- if ($pref) {
- $this->output->writeln('Awesome! Thank you for helping!');
- }
- else {
- // @todo Completely anonymously send an event to indicate some user opted out.
- $this->output->writeln('Ok, no data will be collected and shared with us.');
- $this->output->writeln('We take privacy seriously.');
- $this->output->writeln('If you change your mind, run acli telemetry>.');
- }
- }
- }
-
- public function run(InputInterface $input, OutputInterface $output): int {
- $exitCode = parent::run($input, $output);
- if ($exitCode === 0 && in_array($input->getFirstArgument(), ['self-update', 'update'])) {
- // Exit immediately to avoid loading additional classes breaking updates.
- // @see https://github.com/acquia/cli/issues/218
- return $exitCode;
- }
- $eventProperties = [
- 'app_version' => $this->getApplication()->getVersion(),
- 'arguments' => $input->getArguments(),
- 'exit_code' => $exitCode,
- 'options' => $input->getOptions(),
- 'os_name' => OsInfo::os(),
- 'os_version' => OsInfo::version(),
- 'platform' => OsInfo::family(),
- ];
- Amplitude::getInstance()->queueEvent('Ran command', $eventProperties);
-
- return $exitCode;
- }
-
- /**
- * Add argument and usage examples for applicationUuid.
- */
- protected function acceptApplicationUuid(): static {
- $this->addArgument('applicationUuid', InputArgument::OPTIONAL, 'The Cloud Platform application UUID or alias (i.e. an application name optionally prefixed with the realm)')
- ->addUsage('[]')
- ->addUsage('myapp')
- ->addUsage('prod:myapp')
- ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470');
-
- return $this;
- }
-
- /**
- * Add argument and usage examples for environmentId.
- */
- protected function acceptEnvironmentId(): static {
- $this->addArgument('environmentId', InputArgument::OPTIONAL, 'The Cloud Platform environment ID or alias (i.e. an application and environment name optionally prefixed with the realm)')
- ->addUsage('[]')
- ->addUsage('myapp.dev')
- ->addUsage('prod:myapp.dev')
- ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470');
-
- return $this;
- }
-
- /**
- * Add site argument.
- *
- * Only call this after acceptEnvironmentId() to keep arguments in the expected order.
- *
- * @return $this
- */
- protected function acceptSite(): self {
- // Do not set a default site in order to force a user prompt.
- $this->addArgument('site', InputArgument::OPTIONAL, 'For a multisite application, the directory name of the site')
- ->addUsage('myapp.dev default');
-
- return $this;
- }
-
- /**
- * Prompts the user to choose from a list of available Cloud Platform
- * applications.
- */
- private function promptChooseSubscription(
- Client $acquiaCloudClient
- ): ?SubscriptionResponse {
- $subscriptionsResource = new Subscriptions($acquiaCloudClient);
- $customerSubscriptions = $subscriptionsResource->getAll();
-
- if (!$customerSubscriptions->count()) {
- throw new AcquiaCliException("You have no Cloud subscriptions.");
- }
- return $this->promptChooseFromObjectsOrArrays(
- $customerSubscriptions,
- 'uuid',
- 'name',
- 'Select a Cloud Platform subscription:'
- );
- }
-
- /**
- * Prompts the user to choose from a list of available Cloud Platform
- * applications.
- */
- private function promptChooseApplication(
- Client $acquiaCloudClient
- ): object|array|null {
- $applicationsResource = new Applications($acquiaCloudClient);
- $customerApplications = $applicationsResource->getAll();
-
- if (!$customerApplications->count()) {
- throw new AcquiaCliException("You have no Cloud applications.");
- }
- return $this->promptChooseFromObjectsOrArrays(
- $customerApplications,
- 'uuid',
- 'name',
- 'Select a Cloud Platform application:'
- );
- }
-
- /**
- * Prompts the user to choose from a list of environments for a given Cloud Platform application.
- */
- private function promptChooseEnvironment(
- Client $acquiaCloudClient,
- string $applicationUuid
- ): object|array|null {
- $environmentResource = new Environments($acquiaCloudClient);
- $environments = $environmentResource->getAll($applicationUuid);
- if (!$environments->count()) {
- throw new AcquiaCliException('There are no environments associated with this application.');
- }
- return $this->promptChooseFromObjectsOrArrays(
- $environments,
- 'uuid',
- 'name',
- 'Select a Cloud Platform environment:'
- );
- }
-
- /**
- * Prompts the user to choose from a list of logs for a given Cloud Platform environment.
- */
- protected function promptChooseLogs(): object|array|null {
- $logs = array_map(static function (mixed $logType, mixed $logLabel): array {
- return [
- 'label' => $logLabel,
- 'type' => $logType,
- ];
- }, array_keys(LogstreamManager::AVAILABLE_TYPES), LogstreamManager::AVAILABLE_TYPES);
- return $this->promptChooseFromObjectsOrArrays(
- $logs,
- 'type',
- 'label',
- 'Select one or more logs as a comma-separated list:',
- TRUE
- );
- }
-
- /**
- * Prompt a user to choose from a list.
- *
- * The list is generated from an array of objects. The objects much have at least one unique property and one
- * property that can be used as a human-readable label.
- *
- * @param array[]|object[] $items An array of objects or arrays.
- * @param string $uniqueProperty The property of the $item that will be used to identify the object.
- */
- protected function promptChooseFromObjectsOrArrays(array|ArrayObject $items, string $uniqueProperty, string $labelProperty, string $questionText, bool $multiselect = FALSE): object|array|null {
- $list = [];
- foreach ($items as $item) {
- if (is_array($item)) {
- $list[$item[$uniqueProperty]] = trim($item[$labelProperty]);
- }
- else {
- $list[$item->$uniqueProperty] = trim($item->$labelProperty);
- }
- }
- $labels = array_values($list);
- $default = $multiselect ? 0 : $labels[0];
- $question = new ChoiceQuestion($questionText, $labels, $default);
- $question->setMultiselect($multiselect);
- $choiceId = $this->io->askQuestion($question);
- if (!$multiselect) {
- $identifier = array_search($choiceId, $list, TRUE);
- foreach ($items as $item) {
- if (is_array($item)) {
- if ($item[$uniqueProperty] === $identifier) {
- return $item;
- }
- }
- else if ($item->$uniqueProperty === $identifier) {
- return $item;
- }
- }
- }
- else {
- $chosen = [];
- foreach ($choiceId as $choice) {
- $identifier = array_search($choice, $list, TRUE);
+abstract class CommandBase extends Command implements LoggerAwareInterface
+{
+ use LoggerAwareTrait;
+
+ protected InputInterface $input;
+
+ protected OutputInterface $output;
+
+ protected SymfonyStyle $io;
+ protected FormatterHelper $formatter;
+ private ApplicationResponse $cloudApplication;
+
+ protected string $dir;
+ protected string $localDbUser = 'drupal';
+ protected string $localDbPassword = 'drupal';
+ protected string $localDbName = 'drupal';
+ protected string $localDbHost = 'localhost';
+ protected bool $drushHasActiveDatabaseConnection;
+ protected \GuzzleHttp\Client $updateClient;
+
+ public function __construct(
+ public LocalMachineHelper $localMachineHelper,
+ protected CloudDataStore $datastoreCloud,
+ protected AcquiaCliDatastore $datastoreAcli,
+ protected ApiCredentialsInterface $cloudCredentials,
+ protected TelemetryHelper $telemetryHelper,
+ protected string $projectDir,
+ protected ClientService $cloudApiClientService,
+ public SshHelper $sshHelper,
+ protected string $sshDir,
+ LoggerInterface $logger,
+ ) {
+ $this->logger = $logger;
+ $this->setLocalDbPassword();
+ $this->setLocalDbUser();
+ $this->setLocalDbName();
+ $this->setLocalDbHost();
+ parent::__construct();
+ if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class)) {
+ $this->appendHelp('This command requires authentication via the Cloud Platform API.');
+ }
+ if ((new \ReflectionClass(static::class))->getAttributes(RequireDb::class)) {
+ $this->appendHelp('This command requires an active database connection. Set the following environment variables prior to running this command: '
+ . 'ACLI_DB_HOST, ACLI_DB_NAME, ACLI_DB_USER, ACLI_DB_PASSWORD');
+ }
+ }
+
+ public function appendHelp(string $helpText): void
+ {
+ $currentHelp = $this->getHelp();
+ $helpText = $currentHelp ? $currentHelp . "\n" . $helpText : $currentHelp . $helpText;
+ $this->setHelp($helpText);
+ }
+
+ protected static function getUuidRegexConstraint(): Regex
+ {
+ return new Regex([
+ 'message' => 'This is not a valid UUID.',
+ 'pattern' => '/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i',
+ ]);
+ }
+
+ public function setProjectDir(string $projectDir): void
+ {
+ $this->projectDir = $projectDir;
+ }
+
+ public function getProjectDir(): string
+ {
+ return $this->projectDir;
+ }
+
+ private function setLocalDbUser(): void
+ {
+ if (getenv('ACLI_DB_USER')) {
+ $this->localDbUser = getenv('ACLI_DB_USER');
+ }
+ }
+
+ public function getLocalDbUser(): string
+ {
+ return $this->localDbUser;
+ }
+
+ private function setLocalDbPassword(): void
+ {
+ if (getenv('ACLI_DB_PASSWORD')) {
+ $this->localDbPassword = getenv('ACLI_DB_PASSWORD');
+ }
+ }
+
+ public function getLocalDbPassword(): string
+ {
+ return $this->localDbPassword;
+ }
+
+ private function setLocalDbName(): void
+ {
+ if (getenv('ACLI_DB_NAME')) {
+ $this->localDbName = getenv('ACLI_DB_NAME');
+ }
+ }
+
+ public function getLocalDbName(): string
+ {
+ return $this->localDbName;
+ }
+
+ private function setLocalDbHost(): void
+ {
+ if (getenv('ACLI_DB_HOST')) {
+ $this->localDbHost = getenv('ACLI_DB_HOST');
+ }
+ }
+
+ public function getLocalDbHost(): string
+ {
+ return $this->localDbHost;
+ }
+
+ /**
+ * Initializes the command just after the input has been validated.
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ $this->input = $input;
+ $this->output = $output;
+ $this->io = new SymfonyStyle($input, $output);
+ // Register custom progress bar format.
+ ProgressBar::setFormatDefinition(
+ 'message',
+ "%current%/%max% [%bar%] %percent:3s%% -- %elapsed:6s%/%estimated:-6s%\n %message%"
+ );
+ $this->formatter = $this->getHelper('formatter');
+
+ $this->output->writeln('Acquia CLI version: ' . $this->getApplication()->getVersion(), OutputInterface::VERBOSITY_DEBUG);
+ if (getenv('ACLI_NO_TELEMETRY') !== 'true') {
+ $this->checkAndPromptTelemetryPreference();
+ $this->telemetryHelper->initialize();
+ }
+ $this->checkAuthentication();
+
+ $this->fillMissingRequiredApplicationUuid($input, $output);
+ $this->convertApplicationAliasToUuid($input);
+ $this->convertUserAliasToUuid($input, 'userUuid', 'organizationUuid');
+ $this->convertEnvironmentAliasToUuid($input, 'environmentId');
+ $this->convertEnvironmentAliasToUuid($input, 'source-environment');
+ $this->convertEnvironmentAliasToUuid($input, 'destination-environment');
+ $this->convertEnvironmentAliasToUuid($input, 'source');
+ $this->convertNotificationToUuid($input, 'notificationUuid');
+ $this->convertNotificationToUuid($input, 'notification-uuid');
+
+ if ($latest = $this->checkForNewVersion()) {
+ $this->output->writeln("Acquia CLI $latest is available. Run acli self-update> to update.");
+ }
+ }
+
+ /**
+ * Check if telemetry preference is set, prompt if not.
+ */
+ public function checkAndPromptTelemetryPreference(): void
+ {
+ $sendTelemetry = $this->datastoreCloud->get(DataStoreContract::SEND_TELEMETRY);
+ if ($this->getName() !== 'telemetry' && (!isset($sendTelemetry)) && $this->input->isInteractive()) {
+ $this->output->writeln('We strive to give you the best tools for development.');
+ $this->output->writeln('You can really help us improve by sharing anonymous performance and usage data.');
+ $style = new SymfonyStyle($this->input, $this->output);
+ $pref = $style->confirm('Would you like to share anonymous performance usage and data?');
+ $this->datastoreCloud->set(DataStoreContract::SEND_TELEMETRY, $pref);
+ if ($pref) {
+ $this->output->writeln('Awesome! Thank you for helping!');
+ } else {
+ // @todo Completely anonymously send an event to indicate some user opted out.
+ $this->output->writeln('Ok, no data will be collected and shared with us.');
+ $this->output->writeln('We take privacy seriously.');
+ $this->output->writeln('If you change your mind, run acli telemetry>.');
+ }
+ }
+ }
+
+ public function run(InputInterface $input, OutputInterface $output): int
+ {
+ $exitCode = parent::run($input, $output);
+ if ($exitCode === 0 && in_array($input->getFirstArgument(), ['self-update', 'update'])) {
+ // Exit immediately to avoid loading additional classes breaking updates.
+ // @see https://github.com/acquia/cli/issues/218
+ return $exitCode;
+ }
+ $eventProperties = [
+ 'app_version' => $this->getApplication()->getVersion(),
+ 'arguments' => $input->getArguments(),
+ 'exit_code' => $exitCode,
+ 'options' => $input->getOptions(),
+ 'os_name' => OsInfo::os(),
+ 'os_version' => OsInfo::version(),
+ 'platform' => OsInfo::family(),
+ ];
+ Amplitude::getInstance()->queueEvent('Ran command', $eventProperties);
+
+ return $exitCode;
+ }
+
+ /**
+ * Add argument and usage examples for applicationUuid.
+ */
+ protected function acceptApplicationUuid(): static
+ {
+ $this->addArgument('applicationUuid', InputArgument::OPTIONAL, 'The Cloud Platform application UUID or alias (i.e. an application name optionally prefixed with the realm)')
+ ->addUsage('[]')
+ ->addUsage('myapp')
+ ->addUsage('prod:myapp')
+ ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470');
+
+ return $this;
+ }
+
+ /**
+ * Add argument and usage examples for environmentId.
+ */
+ protected function acceptEnvironmentId(): static
+ {
+ $this->addArgument('environmentId', InputArgument::OPTIONAL, 'The Cloud Platform environment ID or alias (i.e. an application and environment name optionally prefixed with the realm)')
+ ->addUsage('[]')
+ ->addUsage('myapp.dev')
+ ->addUsage('prod:myapp.dev')
+ ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470');
+
+ return $this;
+ }
+
+ /**
+ * Add site argument.
+ *
+ * Only call this after acceptEnvironmentId() to keep arguments in the expected order.
+ *
+ * @return $this
+ */
+ protected function acceptSite(): self
+ {
+ // Do not set a default site in order to force a user prompt.
+ $this->addArgument('site', InputArgument::OPTIONAL, 'For a multisite application, the directory name of the site')
+ ->addUsage('myapp.dev default');
+
+ return $this;
+ }
+
+ /**
+ * Prompts the user to choose from a list of available Cloud Platform
+ * applications.
+ */
+ private function promptChooseSubscription(
+ Client $acquiaCloudClient
+ ): ?SubscriptionResponse {
+ $subscriptionsResource = new Subscriptions($acquiaCloudClient);
+ $customerSubscriptions = $subscriptionsResource->getAll();
+
+ if (!$customerSubscriptions->count()) {
+ throw new AcquiaCliException("You have no Cloud subscriptions.");
+ }
+ return $this->promptChooseFromObjectsOrArrays(
+ $customerSubscriptions,
+ 'uuid',
+ 'name',
+ 'Select a Cloud Platform subscription:'
+ );
+ }
+
+ /**
+ * Prompts the user to choose from a list of available Cloud Platform
+ * applications.
+ */
+ private function promptChooseApplication(
+ Client $acquiaCloudClient
+ ): object|array|null {
+ $applicationsResource = new Applications($acquiaCloudClient);
+ $customerApplications = $applicationsResource->getAll();
+
+ if (!$customerApplications->count()) {
+ throw new AcquiaCliException("You have no Cloud applications.");
+ }
+ return $this->promptChooseFromObjectsOrArrays(
+ $customerApplications,
+ 'uuid',
+ 'name',
+ 'Select a Cloud Platform application:'
+ );
+ }
+
+ /**
+ * Prompts the user to choose from a list of environments for a given Cloud Platform application.
+ */
+ private function promptChooseEnvironment(
+ Client $acquiaCloudClient,
+ string $applicationUuid
+ ): object|array|null {
+ $environmentResource = new Environments($acquiaCloudClient);
+ $environments = $environmentResource->getAll($applicationUuid);
+ if (!$environments->count()) {
+ throw new AcquiaCliException('There are no environments associated with this application.');
+ }
+ return $this->promptChooseFromObjectsOrArrays(
+ $environments,
+ 'uuid',
+ 'name',
+ 'Select a Cloud Platform environment:'
+ );
+ }
+
+ /**
+ * Prompts the user to choose from a list of logs for a given Cloud Platform environment.
+ */
+ protected function promptChooseLogs(): object|array|null
+ {
+ $logs = array_map(static function (mixed $logType, mixed $logLabel): array {
+ return [
+ 'label' => $logLabel,
+ 'type' => $logType,
+ ];
+ }, array_keys(LogstreamManager::AVAILABLE_TYPES), LogstreamManager::AVAILABLE_TYPES);
+ return $this->promptChooseFromObjectsOrArrays(
+ $logs,
+ 'type',
+ 'label',
+ 'Select one or more logs as a comma-separated list:',
+ true
+ );
+ }
+
+ /**
+ * Prompt a user to choose from a list.
+ *
+ * The list is generated from an array of objects. The objects much have at least one unique property and one
+ * property that can be used as a human-readable label.
+ *
+ * @param array[]|object[] $items An array of objects or arrays.
+ * @param string $uniqueProperty The property of the $item that will be used to identify the object.
+ */
+ protected function promptChooseFromObjectsOrArrays(array|ArrayObject $items, string $uniqueProperty, string $labelProperty, string $questionText, bool $multiselect = false): object|array|null
+ {
+ $list = [];
foreach ($items as $item) {
- if (is_array($item)) {
- if ($item[$uniqueProperty] === $identifier) {
- $chosen[] = $item;
+ if (is_array($item)) {
+ $list[$item[$uniqueProperty]] = trim($item[$labelProperty]);
+ } else {
+ $list[$item->$uniqueProperty] = trim($item->$labelProperty);
}
- }
- else if ($item->$uniqueProperty === $identifier) {
- $chosen[] = $item;
- }
- }
- }
- return $chosen;
- }
-
- return NULL;
- }
-
- protected function getHostFromDatabaseResponse(mixed $environment, DatabaseResponse $database): string {
- if ($this->isAcsfEnv($environment)) {
- return $database->db_host . '.enterprise-g1.hosting.acquia.com';
- }
-
- return $database->db_host;
- }
-
- protected function rsyncFiles(string $sourceDir, string $destinationDir, ?callable $outputCallback): void {
- $this->localMachineHelper->checkRequiredBinariesExist(['rsync']);
- $command = [
- 'rsync',
- // -a archive mode; same as -rlptgoD.
- // -z compress file data during the transfer.
- // -v increase verbosity.
- // -P show progress during transfer.
- // -h output numbers in a human-readable format.
- // -e specify the remote shell to use.
- '-avPhze',
- 'ssh -o StrictHostKeyChecking=no',
- $sourceDir . '/',
- $destinationDir,
- ];
- $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to sync files. {message}', ['message' => $process->getErrorOutput()]);
- }
- }
-
- protected function getCloudFilesDir(EnvironmentResponse $chosenEnvironment, string $site): string {
- $sitegroup = self::getSitegroup($chosenEnvironment);
- $envAlias = self::getEnvironmentAlias($chosenEnvironment);
- if ($this->isAcsfEnv($chosenEnvironment)) {
- return "/mnt/files/$envAlias/sites/g/files/$site/files";
- }
- return $this->getCloudSitesPath($chosenEnvironment, $sitegroup) . "/$site/files";
- }
-
- protected function getLocalFilesDir(string $site): string {
- return $this->dir . '/docroot/sites/' . $site . '/files';
- }
-
- /**
- * @param string|null $site
- * @return DatabaseResponse[]
- */
- protected function determineCloudDatabases(Client $acquiaCloudClient, EnvironmentResponse $chosenEnvironment, string $site = NULL, bool $multipleDbs = FALSE): array {
- $databasesRequest = new Databases($acquiaCloudClient);
- $databases = $databasesRequest->getAll($chosenEnvironment->uuid);
-
- if (count($databases) === 1) {
- $this->logger->debug('Only a single database detected on Cloud');
- return [$databases[0]];
- }
- $this->logger->debug('Multiple databases detected on Cloud');
- if ($site && !$multipleDbs) {
- if ($site === 'default') {
- $this->logger->debug('Site is set to default. Assuming default database');
- $site = self::getSitegroup($chosenEnvironment);
- }
- $databaseNames = array_column((array) $databases, 'name');
- $databaseKey = array_search($site, $databaseNames, TRUE);
- if ($databaseKey !== FALSE) {
- return [$databases[$databaseKey]];
- }
- }
- return $this->promptChooseDatabases($chosenEnvironment, $databases, $multipleDbs);
- }
-
- /**
- * @return array
- */
- private function promptChooseDatabases(
- EnvironmentResponse $cloudEnvironment,
- DatabasesResponse $environmentDatabases,
- bool $multipleDbs
- ): array {
- $choices = [];
- if ($multipleDbs) {
- $choices['all'] = 'All';
- }
- $defaultDatabaseIndex = 0;
- if ($this->isAcsfEnv($cloudEnvironment)) {
- $acsfSites = $this->getAcsfSites($cloudEnvironment);
- }
- foreach ($environmentDatabases as $index => $database) {
- $suffix = '';
- if (isset($acsfSites)) {
- foreach ($acsfSites['sites'] as $domain => $acsfSite) {
- if ($acsfSite['conf']['gardens_db_name'] === $database->name) {
- $suffix .= ' (' . $domain . ')';
- break;
- }
- }
- }
- if ($database->flags->default) {
- $defaultDatabaseIndex = $index;
- $suffix .= ' (default)';
- }
- $choices[] = $database->name . $suffix;
- }
-
- $question = new ChoiceQuestion(
- $multipleDbs ? 'Choose databases. You may choose multiple. Use commas to separate choices.' : 'Choose a database.',
- $choices,
- $defaultDatabaseIndex
- );
- $question->setMultiselect($multipleDbs);
- if ($multipleDbs) {
- $chosenDatabaseKeys = $this->io->askQuestion($question);
- $chosenDatabases = [];
- if (count($chosenDatabaseKeys) === 1 && $chosenDatabaseKeys[0] === 'all') {
- if (count($environmentDatabases) > 10) {
- $this->io->warning('You have chosen to pull down more than 10 databases. This could exhaust your disk space.');
- }
- return (array) $environmentDatabases;
- }
- foreach ($chosenDatabaseKeys as $chosenDatabaseKey) {
- $chosenDatabases[] = $environmentDatabases[$chosenDatabaseKey];
- }
-
- return $chosenDatabases;
- }
-
- $chosenDatabaseLabel = $this->io->choice('Choose a database', $choices, $defaultDatabaseIndex);
- $chosenDatabaseIndex = array_search($chosenDatabaseLabel, $choices, TRUE);
- return [$environmentDatabases[$chosenDatabaseIndex]];
- }
-
- protected function determineEnvironment(InputInterface $input, OutputInterface $output, bool $allowProduction = FALSE, bool $allowNode = FALSE): array|string|EnvironmentResponse {
- if ($input->getArgument('environmentId')) {
- $environmentId = $input->getArgument('environmentId');
- $chosenEnvironment = $this->getCloudEnvironment($environmentId);
- }
- else {
- $cloudApplicationUuid = $this->determineCloudApplication();
- $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
- $output->writeln('Using Cloud Application ' . $cloudApplication->name . '>');
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $chosenEnvironment = $this->promptChooseEnvironmentConsiderProd($acquiaCloudClient, $cloudApplicationUuid, $allowProduction, $allowNode);
- }
- $this->logger->debug("Using environment $chosenEnvironment->label $chosenEnvironment->uuid");
-
- return $chosenEnvironment;
- }
-
- // Todo: obviously combine this with promptChooseEnvironment.
- private function promptChooseEnvironmentConsiderProd(Client $acquiaCloudClient, string $applicationUuid, bool $allowProduction, bool $allowNode): EnvironmentResponse {
- $environmentResource = new Environments($acquiaCloudClient);
- $applicationEnvironments = iterator_to_array($environmentResource->getAll($applicationUuid));
- $choices = [];
- foreach ($applicationEnvironments as $key => $environment) {
- $productionNotAllowed = !$allowProduction && $environment->flags->production;
- $nodeNotAllowed = !$allowNode && $environment->type === 'node';
- if ($productionNotAllowed || $nodeNotAllowed) {
- unset($applicationEnvironments[$key]);
- // Re-index array so keys match those in $choices.
- $applicationEnvironments = array_values($applicationEnvironments);
- continue;
- }
- $choices[] = "$environment->label, $environment->name (vcs: {$environment->vcs->path})";
- }
- if (count($choices) === 0) {
- throw new AcquiaCliException('No compatible environments found');
- }
- $chosenEnvironmentLabel = $this->io->choice('Choose a Cloud Platform environment', $choices, $choices[0]);
- $chosenEnvironmentIndex = array_search($chosenEnvironmentLabel, $choices, TRUE);
-
- return $applicationEnvironments[$chosenEnvironmentIndex];
- }
-
- protected function isLocalGitRepoDirty(): bool {
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $process = $this->localMachineHelper->executeFromCmd(
- // Problem with this is that it stages changes for the user. They may
- // not want that.
- 'git add . && git diff-index --cached --quiet HEAD',
- NULL, $this->dir, FALSE);
-
- return !$process->isSuccessful();
- }
-
- protected function getLocalGitCommitHash(): string {
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $process = $this->localMachineHelper->execute([
- 'git',
- 'rev-parse',
- 'HEAD',
- ], NULL, $this->dir, FALSE);
-
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Unable to determine Git commit hash.");
- }
-
- return trim($process->getOutput());
- }
-
- /**
- * Load configuration from .git/config.
- *
- * @return string[][]
- * A multidimensional array keyed by file section.
- */
- private function getGitConfig(): array {
- $filePath = $this->projectDir . '/.git/config';
- return @\Safe\parse_ini_file($filePath, TRUE);
- }
-
- /**
- * Gets an array of git remotes from a .git/config array.
- *
- * @param string[][] $gitConfig
- * @return string[]
- * A flat array of git remote urls.
- */
- private function getGitRemotes(array $gitConfig): array {
- $localVcsRemotes = [];
- foreach ($gitConfig as $sectionName => $section) {
- if ((str_contains($sectionName, 'remote ')) &&
- array_key_exists('url', $section) &&
- (strpos($section['url'], 'acquia.com') || strpos($section['url'], 'acquia-sites.com'))
- ) {
- $localVcsRemotes[] = $section['url'];
- }
- }
-
- return $localVcsRemotes;
- }
-
- private function findCloudApplicationByGitUrl(
+ }
+ $labels = array_values($list);
+ $default = $multiselect ? 0 : $labels[0];
+ $question = new ChoiceQuestion($questionText, $labels, $default);
+ $question->setMultiselect($multiselect);
+ $choiceId = $this->io->askQuestion($question);
+ if (!$multiselect) {
+ $identifier = array_search($choiceId, $list, true);
+ foreach ($items as $item) {
+ if (is_array($item)) {
+ if ($item[$uniqueProperty] === $identifier) {
+ return $item;
+ }
+ } elseif ($item->$uniqueProperty === $identifier) {
+ return $item;
+ }
+ }
+ } else {
+ $chosen = [];
+ foreach ($choiceId as $choice) {
+ $identifier = array_search($choice, $list, true);
+ foreach ($items as $item) {
+ if (is_array($item)) {
+ if ($item[$uniqueProperty] === $identifier) {
+ $chosen[] = $item;
+ }
+ } elseif ($item->$uniqueProperty === $identifier) {
+ $chosen[] = $item;
+ }
+ }
+ }
+ return $chosen;
+ }
+
+ return null;
+ }
+
+ protected function getHostFromDatabaseResponse(mixed $environment, DatabaseResponse $database): string
+ {
+ if ($this->isAcsfEnv($environment)) {
+ return $database->db_host . '.enterprise-g1.hosting.acquia.com';
+ }
+
+ return $database->db_host;
+ }
+
+ protected function rsyncFiles(string $sourceDir, string $destinationDir, ?callable $outputCallback): void
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['rsync']);
+ $command = [
+ 'rsync',
+ // -a archive mode; same as -rlptgoD.
+ // -z compress file data during the transfer.
+ // -v increase verbosity.
+ // -P show progress during transfer.
+ // -h output numbers in a human-readable format.
+ // -e specify the remote shell to use.
+ '-avPhze',
+ 'ssh -o StrictHostKeyChecking=no',
+ $sourceDir . '/',
+ $destinationDir,
+ ];
+ $process = $this->localMachineHelper->execute($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to sync files. {message}', ['message' => $process->getErrorOutput()]);
+ }
+ }
+
+ protected function getCloudFilesDir(EnvironmentResponse $chosenEnvironment, string $site): string
+ {
+ $sitegroup = self::getSitegroup($chosenEnvironment);
+ $envAlias = self::getEnvironmentAlias($chosenEnvironment);
+ if ($this->isAcsfEnv($chosenEnvironment)) {
+ return "/mnt/files/$envAlias/sites/g/files/$site/files";
+ }
+ return $this->getCloudSitesPath($chosenEnvironment, $sitegroup) . "/$site/files";
+ }
+
+ protected function getLocalFilesDir(string $site): string
+ {
+ return $this->dir . '/docroot/sites/' . $site . '/files';
+ }
+
+ /**
+ * @param string|null $site
+ * @return DatabaseResponse[]
+ */
+ protected function determineCloudDatabases(Client $acquiaCloudClient, EnvironmentResponse $chosenEnvironment, string $site = null, bool $multipleDbs = false): array
+ {
+ $databasesRequest = new Databases($acquiaCloudClient);
+ $databases = $databasesRequest->getAll($chosenEnvironment->uuid);
+
+ if (count($databases) === 1) {
+ $this->logger->debug('Only a single database detected on Cloud');
+ return [$databases[0]];
+ }
+ $this->logger->debug('Multiple databases detected on Cloud');
+ if ($site && !$multipleDbs) {
+ if ($site === 'default') {
+ $this->logger->debug('Site is set to default. Assuming default database');
+ $site = self::getSitegroup($chosenEnvironment);
+ }
+ $databaseNames = array_column((array) $databases, 'name');
+ $databaseKey = array_search($site, $databaseNames, true);
+ if ($databaseKey !== false) {
+ return [$databases[$databaseKey]];
+ }
+ }
+ return $this->promptChooseDatabases($chosenEnvironment, $databases, $multipleDbs);
+ }
+
+ /**
+ * @return array
+ */
+ private function promptChooseDatabases(
+ EnvironmentResponse $cloudEnvironment,
+ DatabasesResponse $environmentDatabases,
+ bool $multipleDbs
+ ): array {
+ $choices = [];
+ if ($multipleDbs) {
+ $choices['all'] = 'All';
+ }
+ $defaultDatabaseIndex = 0;
+ if ($this->isAcsfEnv($cloudEnvironment)) {
+ $acsfSites = $this->getAcsfSites($cloudEnvironment);
+ }
+ foreach ($environmentDatabases as $index => $database) {
+ $suffix = '';
+ if (isset($acsfSites)) {
+ foreach ($acsfSites['sites'] as $domain => $acsfSite) {
+ if ($acsfSite['conf']['gardens_db_name'] === $database->name) {
+ $suffix .= ' (' . $domain . ')';
+ break;
+ }
+ }
+ }
+ if ($database->flags->default) {
+ $defaultDatabaseIndex = $index;
+ $suffix .= ' (default)';
+ }
+ $choices[] = $database->name . $suffix;
+ }
+
+ $question = new ChoiceQuestion(
+ $multipleDbs ? 'Choose databases. You may choose multiple. Use commas to separate choices.' : 'Choose a database.',
+ $choices,
+ $defaultDatabaseIndex
+ );
+ $question->setMultiselect($multipleDbs);
+ if ($multipleDbs) {
+ $chosenDatabaseKeys = $this->io->askQuestion($question);
+ $chosenDatabases = [];
+ if (count($chosenDatabaseKeys) === 1 && $chosenDatabaseKeys[0] === 'all') {
+ if (count($environmentDatabases) > 10) {
+ $this->io->warning('You have chosen to pull down more than 10 databases. This could exhaust your disk space.');
+ }
+ return (array) $environmentDatabases;
+ }
+ foreach ($chosenDatabaseKeys as $chosenDatabaseKey) {
+ $chosenDatabases[] = $environmentDatabases[$chosenDatabaseKey];
+ }
+
+ return $chosenDatabases;
+ }
+
+ $chosenDatabaseLabel = $this->io->choice('Choose a database', $choices, $defaultDatabaseIndex);
+ $chosenDatabaseIndex = array_search($chosenDatabaseLabel, $choices, true);
+ return [$environmentDatabases[$chosenDatabaseIndex]];
+ }
+
+ protected function determineEnvironment(InputInterface $input, OutputInterface $output, bool $allowProduction = false, bool $allowNode = false): array|string|EnvironmentResponse
+ {
+ if ($input->getArgument('environmentId')) {
+ $environmentId = $input->getArgument('environmentId');
+ $chosenEnvironment = $this->getCloudEnvironment($environmentId);
+ } else {
+ $cloudApplicationUuid = $this->determineCloudApplication();
+ $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
+ $output->writeln('Using Cloud Application ' . $cloudApplication->name . '>');
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $chosenEnvironment = $this->promptChooseEnvironmentConsiderProd($acquiaCloudClient, $cloudApplicationUuid, $allowProduction, $allowNode);
+ }
+ $this->logger->debug("Using environment $chosenEnvironment->label $chosenEnvironment->uuid");
+
+ return $chosenEnvironment;
+ }
+
+ // Todo: obviously combine this with promptChooseEnvironment.
+ private function promptChooseEnvironmentConsiderProd(Client $acquiaCloudClient, string $applicationUuid, bool $allowProduction, bool $allowNode): EnvironmentResponse
+ {
+ $environmentResource = new Environments($acquiaCloudClient);
+ $applicationEnvironments = iterator_to_array($environmentResource->getAll($applicationUuid));
+ $choices = [];
+ foreach ($applicationEnvironments as $key => $environment) {
+ $productionNotAllowed = !$allowProduction && $environment->flags->production;
+ $nodeNotAllowed = !$allowNode && $environment->type === 'node';
+ if ($productionNotAllowed || $nodeNotAllowed) {
+ unset($applicationEnvironments[$key]);
+ // Re-index array so keys match those in $choices.
+ $applicationEnvironments = array_values($applicationEnvironments);
+ continue;
+ }
+ $choices[] = "$environment->label, $environment->name (vcs: {$environment->vcs->path})";
+ }
+ if (count($choices) === 0) {
+ throw new AcquiaCliException('No compatible environments found');
+ }
+ $chosenEnvironmentLabel = $this->io->choice('Choose a Cloud Platform environment', $choices, $choices[0]);
+ $chosenEnvironmentIndex = array_search($chosenEnvironmentLabel, $choices, true);
+
+ return $applicationEnvironments[$chosenEnvironmentIndex];
+ }
+
+ protected function isLocalGitRepoDirty(): bool
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $process = $this->localMachineHelper->executeFromCmd(
+ // Problem with this is that it stages changes for the user. They may
+ // not want that.
+ 'git add . && git diff-index --cached --quiet HEAD',
+ null,
+ $this->dir,
+ false
+ );
+
+ return !$process->isSuccessful();
+ }
+
+ protected function getLocalGitCommitHash(): string
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $process = $this->localMachineHelper->execute([
+ 'git',
+ 'rev-parse',
+ 'HEAD',
+ ], null, $this->dir, false);
+
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Unable to determine Git commit hash.");
+ }
+
+ return trim($process->getOutput());
+ }
+
+ /**
+ * Load configuration from .git/config.
+ *
+ * @return string[][]
+ * A multidimensional array keyed by file section.
+ */
+ private function getGitConfig(): array
+ {
+ $filePath = $this->projectDir . '/.git/config';
+ return @\Safe\parse_ini_file($filePath, true);
+ }
+
+ /**
+ * Gets an array of git remotes from a .git/config array.
+ *
+ * @param string[][] $gitConfig
+ * @return string[]
+ * A flat array of git remote urls.
+ */
+ private function getGitRemotes(array $gitConfig): array
+ {
+ $localVcsRemotes = [];
+ foreach ($gitConfig as $sectionName => $section) {
+ if (
+ (str_contains($sectionName, 'remote ')) &&
+ array_key_exists('url', $section) &&
+ (strpos($section['url'], 'acquia.com') || strpos($section['url'], 'acquia-sites.com'))
+ ) {
+ $localVcsRemotes[] = $section['url'];
+ }
+ }
+
+ return $localVcsRemotes;
+ }
+
+ private function findCloudApplicationByGitUrl(
Client $acquiaCloudClient,
array $localGitRemotes
): ?ApplicationResponse {
- // Set up API resources.
- $applicationsResource = new Applications($acquiaCloudClient);
- $customerApplications = $applicationsResource->getAll();
- $environmentsResource = new Environments($acquiaCloudClient);
-
- // Create progress bar.
- $count = count($customerApplications);
- $progressBar = new ProgressBar($this->output, $count);
- $progressBar->setFormat('message');
- $progressBar->setMessage("Searching $count applications> on the Cloud Platform...");
- $progressBar->start();
-
- // Search Cloud applications.
- $terminalWidth = (new Terminal())->getWidth();
- foreach ($customerApplications as $application) {
- // Ensure that the message takes up the full terminal width to prevent display artifacts.
- $message = "Searching {$application->name}> for matching git URLs";
- $suffixLength = $terminalWidth - strlen($message) - 17;
- $suffix = $suffixLength > 0 ? str_repeat(' ', $suffixLength) : '';
- $progressBar->setMessage($message . $suffix);
- $applicationEnvironments = $environmentsResource->getAll($application->uuid);
- if ($application = $this->searchApplicationEnvironmentsForGitUrl(
- $application,
- $applicationEnvironments,
- $localGitRemotes
- )) {
+ // Set up API resources.
+ $applicationsResource = new Applications($acquiaCloudClient);
+ $customerApplications = $applicationsResource->getAll();
+ $environmentsResource = new Environments($acquiaCloudClient);
+
+ // Create progress bar.
+ $count = count($customerApplications);
+ $progressBar = new ProgressBar($this->output, $count);
+ $progressBar->setFormat('message');
+ $progressBar->setMessage("Searching $count applications> on the Cloud Platform...");
+ $progressBar->start();
+
+ // Search Cloud applications.
+ $terminalWidth = (new Terminal())->getWidth();
+ foreach ($customerApplications as $application) {
+ // Ensure that the message takes up the full terminal width to prevent display artifacts.
+ $message = "Searching {$application->name}> for matching git URLs";
+ $suffixLength = $terminalWidth - strlen($message) - 17;
+ $suffix = $suffixLength > 0 ? str_repeat(' ', $suffixLength) : '';
+ $progressBar->setMessage($message . $suffix);
+ $applicationEnvironments = $environmentsResource->getAll($application->uuid);
+ if (
+ $application = $this->searchApplicationEnvironmentsForGitUrl(
+ $application,
+ $applicationEnvironments,
+ $localGitRemotes
+ )
+ ) {
+ $progressBar->finish();
+ $progressBar->clear();
+
+ return $application;
+ }
+ $progressBar->advance();
+ }
$progressBar->finish();
$progressBar->clear();
- return $application;
- }
- $progressBar->advance();
- }
- $progressBar->finish();
- $progressBar->clear();
-
- return NULL;
- }
-
- protected function createTable(OutputInterface $output, string $title, array $headers, mixed $widths): Table {
- $terminalWidth = (new Terminal())->getWidth();
- $terminalWidth *= .90;
- $table = new Table($output);
- $table->setHeaders($headers);
- $table->setHeaderTitle($title);
- $setWidths = static function (mixed $width) use ($terminalWidth) {
- return (int) ($terminalWidth * $width);
- };
- $table->setColumnWidths(array_map($setWidths, $widths));
- return $table;
- }
-
- private function searchApplicationEnvironmentsForGitUrl(
- ApplicationResponse $application,
- EnvironmentsResponse $applicationEnvironments,
- array $localGitRemotes
- ): ?ApplicationResponse {
- foreach ($applicationEnvironments as $environment) {
- if ($environment->flags->production && in_array($environment->vcs->url, $localGitRemotes, TRUE)) {
- $this->logger->debug("Found matching Cloud application! {$application->name} with uuid {$application->uuid} matches local git URL {$environment->vcs->url}");
+ return null;
+ }
- return $application;
- }
+ protected function createTable(OutputInterface $output, string $title, array $headers, mixed $widths): Table
+ {
+ $terminalWidth = (new Terminal())->getWidth();
+ $terminalWidth *= .90;
+ $table = new Table($output);
+ $table->setHeaders($headers);
+ $table->setHeaderTitle($title);
+ $setWidths = static function (mixed $width) use ($terminalWidth) {
+ return (int) ($terminalWidth * $width);
+ };
+ $table->setColumnWidths(array_map($setWidths, $widths));
+ return $table;
}
- return NULL;
- }
+ private function searchApplicationEnvironmentsForGitUrl(
+ ApplicationResponse $application,
+ EnvironmentsResponse $applicationEnvironments,
+ array $localGitRemotes
+ ): ?ApplicationResponse {
+ foreach ($applicationEnvironments as $environment) {
+ if ($environment->flags->production && in_array($environment->vcs->url, $localGitRemotes, true)) {
+ $this->logger->debug("Found matching Cloud application! {$application->name} with uuid {$application->uuid} matches local git URL {$environment->vcs->url}");
+
+ return $application;
+ }
+ }
+
+ return null;
+ }
- /**
- * Infer which Cloud Platform application is associated with the current local git repository.
- *
- * If the local git repository has a remote with a URL that matches a Cloud Platform application's VCS URL, assume
- * that we have a match.
- */
- protected function inferCloudAppFromLocalGitConfig(
- Client $acquiaCloudClient
+ /**
+ * Infer which Cloud Platform application is associated with the current local git repository.
+ *
+ * If the local git repository has a remote with a URL that matches a Cloud Platform application's VCS URL, assume
+ * that we have a match.
+ */
+ protected function inferCloudAppFromLocalGitConfig(
+ Client $acquiaCloudClient
): ?ApplicationResponse {
- if ($this->projectDir && $this->input->isInteractive()) {
- $this->output->writeln("There is no Cloud Platform application linked to {$this->projectDir}/.git>.");
- $answer = $this->io->confirm('Would you like Acquia CLI to search for a Cloud application that matches your local git config?');
- if ($answer) {
- $this->output->writeln('Searching for a matching Cloud application...');
- try {
- $gitConfig = $this->getGitConfig();
- $localGitRemotes = $this->getGitRemotes($gitConfig);
- if ($cloudApplication = $this->findCloudApplicationByGitUrl($acquiaCloudClient,
- $localGitRemotes)) {
- $this->output->writeln('Found a matching application!');
- return $cloudApplication;
- }
+ if ($this->projectDir && $this->input->isInteractive()) {
+ $this->output->writeln("There is no Cloud Platform application linked to {$this->projectDir}/.git>.");
+ $answer = $this->io->confirm('Would you like Acquia CLI to search for a Cloud application that matches your local git config?');
+ if ($answer) {
+ $this->output->writeln('Searching for a matching Cloud application...');
+ try {
+ $gitConfig = $this->getGitConfig();
+ $localGitRemotes = $this->getGitRemotes($gitConfig);
+ if (
+ $cloudApplication = $this->findCloudApplicationByGitUrl(
+ $acquiaCloudClient,
+ $localGitRemotes
+ )
+ ) {
+ $this->output->writeln('Found a matching application!');
+ return $cloudApplication;
+ }
+
+ $this->output->writeln('Could not find a matching Cloud application.');
+ return null;
+ } catch (FilesystemException $e) {
+ throw new AcquiaCliException($e->getMessage());
+ }
+ }
+ }
+
+ return null;
+ }
- $this->output->writeln('Could not find a matching Cloud application.');
- return NULL;
+ /**
+ * @return array
+ */
+ protected function getSubscriptionApplications(Client $client, SubscriptionResponse $subscription): array
+ {
+ $applicationsResource = new Applications($client);
+ $applications = $applicationsResource->getAll();
+ $subscriptionApplications = [];
+
+ foreach ($applications as $application) {
+ if ($application->subscription->uuid === $subscription->uuid) {
+ $subscriptionApplications[] = $application;
+ }
}
- catch (FilesystemException $e) {
- throw new AcquiaCliException($e->getMessage());
+ if (count($subscriptionApplications) === 0) {
+ throw new AcquiaCliException("You do not have access to any applications on the $subscription->name subscription");
}
- }
+ return $subscriptionApplications;
}
- return NULL;
- }
+ protected function determineCloudSubscription(): SubscriptionResponse
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
- /**
- * @return array
- */
- protected function getSubscriptionApplications(Client $client, SubscriptionResponse $subscription): array {
- $applicationsResource = new Applications($client);
- $applications = $applicationsResource->getAll();
- $subscriptionApplications = [];
+ if ($this->input->hasArgument('subscriptionUuid') && $this->input->getArgument('subscriptionUuid')) {
+ $cloudSubscriptionUuid = $this->input->getArgument('subscriptionUuid');
+ self::validateUuid($cloudSubscriptionUuid);
+ return (new Subscriptions($acquiaCloudClient))->get($cloudSubscriptionUuid);
+ }
+
+ // Finally, just ask.
+ if ($this->input->isInteractive() && $subscription = $this->promptChooseSubscription($acquiaCloudClient)) {
+ return $subscription;
+ }
- foreach ($applications as $application) {
- if ($application->subscription->uuid === $subscription->uuid) {
- $subscriptionApplications[] = $application;
- }
+ throw new AcquiaCliException("Could not determine Cloud subscription. Run this command interactively or use the `subscriptionUuid` argument.");
}
- if (count($subscriptionApplications) === 0) {
- throw new AcquiaCliException("You do not have access to any applications on the $subscription->name subscription");
+
+ /**
+ * Determine the Cloud application.
+ */
+ protected function determineCloudApplication(bool $promptLinkApp = false): ?string
+ {
+ $applicationUuid = $this->doDetermineCloudApplication();
+ if (!isset($applicationUuid)) {
+ throw new AcquiaCliException("Could not determine Cloud Application. Run this command interactively or use `acli link` to link a Cloud Application before running non-interactively.");
+ }
+
+ $application = $this->getCloudApplication($applicationUuid);
+ // No point in trying to link a directory that's not a repo.
+ if (!empty($this->projectDir) && !$this->getCloudUuidFromDatastore()) {
+ if ($promptLinkApp) {
+ $this->saveCloudUuidToDatastore($application);
+ } elseif (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->getCloudApplicationUuidFromBltYaml()) {
+ $this->promptLinkApplication($application);
+ }
+ }
+
+ return $applicationUuid;
}
- return $subscriptionApplications;
- }
- protected function determineCloudSubscription(): SubscriptionResponse {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ protected function doDetermineCloudApplication(): ?string
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+
+ if ($this->input->hasArgument('applicationUuid') && $this->input->getArgument('applicationUuid')) {
+ $cloudApplicationUuid = $this->input->getArgument('applicationUuid');
+ return self::validateUuid($cloudApplicationUuid);
+ }
+
+ if ($this->input->hasArgument('environmentId') && $this->input->getArgument('environmentId')) {
+ $environmentId = $this->input->getArgument('environmentId');
+ $environment = $this->getCloudEnvironment($environmentId);
+ return $environment->application->uuid;
+ }
+
+ // Try local project info.
+ if ($applicationUuid = $this->getCloudUuidFromDatastore()) {
+ $this->logger->debug("Using Cloud application UUID: $applicationUuid from {$this->datastoreAcli->filepath}");
+ return $applicationUuid;
+ }
+
+ if ($applicationUuid = $this->getCloudApplicationUuidFromBltYaml()) {
+ $this->logger->debug("Using Cloud application UUID $applicationUuid from blt/blt.yml");
+ return $applicationUuid;
+ }
+
+ // Get from the Cloud Platform env var.
+ if ($applicationUuid = self::getThisCloudIdeCloudAppUuid()) {
+ return $applicationUuid;
+ }
+
+ // Try to guess based on local git url config.
+ if ($cloudApplication = $this->inferCloudAppFromLocalGitConfig($acquiaCloudClient)) {
+ return $cloudApplication->uuid;
+ }
- if ($this->input->hasArgument('subscriptionUuid') && $this->input->getArgument('subscriptionUuid')) {
- $cloudSubscriptionUuid = $this->input->getArgument('subscriptionUuid');
- self::validateUuid($cloudSubscriptionUuid);
- return (new Subscriptions($acquiaCloudClient))->get($cloudSubscriptionUuid);
+ if ($this->input->isInteractive()) {
+ /** @var ApplicationResponse $application */
+ $application = $this->promptChooseApplication($acquiaCloudClient);
+ if ($application) {
+ return $application->uuid;
+ }
+ }
+
+ return null;
}
- // Finally, just ask.
- if ($this->input->isInteractive() && $subscription = $this->promptChooseSubscription($acquiaCloudClient)) {
- return $subscription;
+ protected function getCloudApplicationUuidFromBltYaml(): ?string
+ {
+ $bltYamlFilePath = Path::join($this->projectDir, 'blt', 'blt.yml');
+ if (file_exists($bltYamlFilePath)) {
+ $contents = Yaml::parseFile($bltYamlFilePath);
+ if (array_key_exists('cloud', $contents) && array_key_exists('appId', $contents['cloud'])) {
+ return $contents['cloud']['appId'];
+ }
+ }
+
+ return null;
}
- throw new AcquiaCliException("Could not determine Cloud subscription. Run this command interactively or use the `subscriptionUuid` argument.");
- }
+ public static function validateUuid(string $uuid): string
+ {
+ $violations = Validation::createValidator()->validate($uuid, [
+ new Length([
+ 'value' => 36,
+ ]),
+ self::getUuidRegexConstraint(),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
- /**
- * Determine the Cloud application.
- */
- protected function determineCloudApplication(bool $promptLinkApp = FALSE): ?string {
- $applicationUuid = $this->doDetermineCloudApplication();
- if (!isset($applicationUuid)) {
- throw new AcquiaCliException("Could not determine Cloud Application. Run this command interactively or use `acli link` to link a Cloud Application before running non-interactively.");
+ return $uuid;
}
- $application = $this->getCloudApplication($applicationUuid);
- // No point in trying to link a directory that's not a repo.
- if (!empty($this->projectDir) && !$this->getCloudUuidFromDatastore()) {
- if ($promptLinkApp) {
- $this->saveCloudUuidToDatastore($application);
- }
- elseif (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->getCloudApplicationUuidFromBltYaml()) {
- $this->promptLinkApplication($application);
- }
+ private function saveCloudUuidToDatastore(ApplicationResponse $application): bool
+ {
+ $this->datastoreAcli->set('cloud_app_uuid', $application->uuid);
+ $this->io->success("The Cloud application {$application->name} has been linked to this repository by writing to {$this->datastoreAcli->filepath}");
+
+ return true;
}
- return $applicationUuid;
- }
+ protected function getCloudUuidFromDatastore(): ?string
+ {
+ return $this->datastoreAcli->get('cloud_app_uuid');
+ }
- protected function doDetermineCloudApplication(): ?string {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ private function promptLinkApplication(
+ ApplicationResponse $cloudApplication
+ ): bool {
+ $answer = $this->io->confirm("Would you like to link the Cloud application $cloudApplication->name> to this repository?");
+ if ($answer) {
+ return $this->saveCloudUuidToDatastore($cloudApplication);
+ }
+ return false;
+ }
- if ($this->input->hasArgument('applicationUuid') && $this->input->getArgument('applicationUuid')) {
- $cloudApplicationUuid = $this->input->getArgument('applicationUuid');
- return self::validateUuid($cloudApplicationUuid);
+ protected function validateCwdIsValidDrupalProject(): void
+ {
+ if (!$this->projectDir) {
+ throw new AcquiaCliException('Could not find a local Drupal project. Looked for `docroot/index.php` in current and parent directories. Execute this command from within a Drupal project directory.');
+ }
}
- if ($this->input->hasArgument('environmentId') && $this->input->getArgument('environmentId')) {
- $environmentId = $this->input->getArgument('environmentId');
- $environment = $this->getCloudEnvironment($environmentId);
- return $environment->application->uuid;
+ /**
+ * Determines if Acquia CLI is being run from within a Cloud IDE.
+ *
+ * @return bool TRUE if Acquia CLI is being run from within a Cloud IDE.
+ */
+ public static function isAcquiaCloudIde(): bool
+ {
+ return AcquiaDrupalEnvironmentDetector::isAhIdeEnv();
}
- // Try local project info.
- if ($applicationUuid = $this->getCloudUuidFromDatastore()) {
- $this->logger->debug("Using Cloud application UUID: $applicationUuid from {$this->datastoreAcli->filepath}");
- return $applicationUuid;
+ /**
+ * Get the Cloud Application UUID from a Cloud IDE's environmental variable.
+ *
+ * This command assumes it is being run inside a Cloud IDE.
+ */
+ protected static function getThisCloudIdeCloudAppUuid(): false|string
+ {
+ return getenv('ACQUIA_APPLICATION_UUID');
}
- if ($applicationUuid = $this->getCloudApplicationUuidFromBltYaml()) {
- $this->logger->debug("Using Cloud application UUID $applicationUuid from blt/blt.yml");
- return $applicationUuid;
+ /**
+ * Get the UUID from a Cloud IDE's environmental variable.
+ *
+ * This command assumes it is being run inside a Cloud IDE.
+ */
+ protected static function getThisCloudIdeUuid(): false|string
+ {
+ return getenv('REMOTEIDE_UUID');
}
- // Get from the Cloud Platform env var.
- if ($applicationUuid = self::getThisCloudIdeCloudAppUuid()) {
- return $applicationUuid;
+ protected static function getThisCloudIdeLabel(): false|string
+ {
+ return getenv('REMOTEIDE_LABEL');
}
- // Try to guess based on local git url config.
- if ($cloudApplication = $this->inferCloudAppFromLocalGitConfig($acquiaCloudClient)) {
- return $cloudApplication->uuid;
+ protected static function getThisCloudIdeWebUrl(): false|string
+ {
+ return getenv('REMOTEIDE_WEB_HOST');
}
- if ($this->input->isInteractive()) {
- /** @var ApplicationResponse $application */
- $application = $this->promptChooseApplication($acquiaCloudClient);
- if ($application) {
- return $application->uuid;
- }
+ protected function getCloudApplication(string $applicationUuid): ApplicationResponse
+ {
+ $applicationsResource = new Applications($this->cloudApiClientService->getClient());
+ return $applicationsResource->get($applicationUuid);
}
- return NULL;
- }
+ protected function getCloudEnvironment(string $environmentId): EnvironmentResponse
+ {
+ $environmentResource = new Environments($this->cloudApiClientService->getClient());
- protected function getCloudApplicationUuidFromBltYaml(): ?string {
- $bltYamlFilePath = Path::join($this->projectDir, 'blt', 'blt.yml');
- if (file_exists($bltYamlFilePath)) {
- $contents = Yaml::parseFile($bltYamlFilePath);
- if (array_key_exists('cloud', $contents) && array_key_exists('appId', $contents['cloud'])) {
- return $contents['cloud']['appId'];
- }
+ return $environmentResource->get($environmentId);
}
- return NULL;
- }
+ public static function validateEnvironmentAlias(string $alias): string
+ {
+ $violations = Validation::createValidator()->validate($alias, [
+ new Length(['min' => 5]),
+ new NotBlank(),
+ new Regex(['pattern' => '/.+\..+/', 'message' => 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]']),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+
+ return $alias;
+ }
- public static function validateUuid(string $uuid): string {
- $violations = Validation::createValidator()->validate($uuid, [
- new Length([
- 'value' => 36,
- ]),
- self::getUuidRegexConstraint(),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+ protected function normalizeAlias(string $alias): string
+ {
+ return str_replace('@', '', $alias);
+ }
+
+ protected function getEnvironmentFromAliasArg(string $alias): EnvironmentResponse
+ {
+ return $this->getEnvFromAlias($alias);
}
- return $uuid;
- }
+ private function getEnvFromAlias(string $alias): EnvironmentResponse
+ {
+ return self::getAliasCache()->get($alias, function () use ($alias): \AcquiaCloudApi\Response\EnvironmentResponse {
+ return $this->doGetEnvFromAlias($alias);
+ });
+ }
- private function saveCloudUuidToDatastore(ApplicationResponse $application): bool {
- $this->datastoreAcli->set('cloud_app_uuid', $application->uuid);
- $this->io->success("The Cloud application {$application->name} has been linked to this repository by writing to {$this->datastoreAcli->filepath}");
+ private function doGetEnvFromAlias(string $alias): EnvironmentResponse
+ {
+ $siteEnvParts = explode('.', $alias);
+ [$applicationAlias, $environmentAlias] = $siteEnvParts;
+ $this->logger->debug("Searching for an environment matching alias $applicationAlias.$environmentAlias.");
+ $customerApplication = $this->getApplicationFromAlias($applicationAlias);
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $environmentsResource = new Environments($acquiaCloudClient);
+ $environments = $environmentsResource->getAll($customerApplication->uuid);
+ foreach ($environments as $environment) {
+ if ($environment->name === $environmentAlias) {
+ $this->logger->debug("Found environment {$environment->uuid} matching $environmentAlias.");
- return TRUE;
- }
+ return $environment;
+ }
+ }
- protected function getCloudUuidFromDatastore(): ?string {
- return $this->datastoreAcli->get('cloud_app_uuid');
- }
+ throw new AcquiaCliException("Environment not found matching the alias {alias}", ['alias' => "$applicationAlias.$environmentAlias"]);
+ }
- private function promptLinkApplication(
- ApplicationResponse $cloudApplication
- ): bool {
- $answer = $this->io->confirm("Would you like to link the Cloud application $cloudApplication->name> to this repository?");
- if ($answer) {
- return $this->saveCloudUuidToDatastore($cloudApplication);
- }
- return FALSE;
- }
-
- protected function validateCwdIsValidDrupalProject(): void {
- if (!$this->projectDir) {
- throw new AcquiaCliException('Could not find a local Drupal project. Looked for `docroot/index.php` in current and parent directories. Execute this command from within a Drupal project directory.');
- }
- }
-
- /**
- * Determines if Acquia CLI is being run from within a Cloud IDE.
- *
- * @return bool TRUE if Acquia CLI is being run from within a Cloud IDE.
- */
- public static function isAcquiaCloudIde(): bool {
- return AcquiaDrupalEnvironmentDetector::isAhIdeEnv();
- }
-
- /**
- * Get the Cloud Application UUID from a Cloud IDE's environmental variable.
- *
- * This command assumes it is being run inside a Cloud IDE.
- */
- protected static function getThisCloudIdeCloudAppUuid(): false|string {
- return getenv('ACQUIA_APPLICATION_UUID');
- }
-
- /**
- * Get the UUID from a Cloud IDE's environmental variable.
- *
- * This command assumes it is being run inside a Cloud IDE.
- */
- protected static function getThisCloudIdeUuid(): false|string {
- return getenv('REMOTEIDE_UUID');
- }
-
- protected static function getThisCloudIdeLabel(): false|string {
- return getenv('REMOTEIDE_LABEL');
- }
-
- protected static function getThisCloudIdeWebUrl(): false|string {
- return getenv('REMOTEIDE_WEB_HOST');
- }
-
- protected function getCloudApplication(string $applicationUuid): ApplicationResponse {
- $applicationsResource = new Applications($this->cloudApiClientService->getClient());
- return $applicationsResource->get($applicationUuid);
- }
-
- protected function getCloudEnvironment(string $environmentId): EnvironmentResponse {
- $environmentResource = new Environments($this->cloudApiClientService->getClient());
-
- return $environmentResource->get($environmentId);
- }
-
- public static function validateEnvironmentAlias(string $alias): string {
- $violations = Validation::createValidator()->validate($alias, [
- new Length(['min' => 5]),
- new NotBlank(),
- new Regex(['pattern' => '/.+\..+/', 'message' => 'You must enter either an environment ID or alias. Environment aliases must match the pattern [app-name].[env]']),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
- }
-
- return $alias;
- }
-
- protected function normalizeAlias(string $alias): string {
- return str_replace('@', '', $alias);
- }
-
- protected function getEnvironmentFromAliasArg(string $alias): EnvironmentResponse {
- return $this->getEnvFromAlias($alias);
- }
-
- private function getEnvFromAlias(string $alias): EnvironmentResponse {
- return self::getAliasCache()->get($alias, function () use ($alias): \AcquiaCloudApi\Response\EnvironmentResponse {
- return $this->doGetEnvFromAlias($alias);
- });
- }
-
- private function doGetEnvFromAlias(string $alias): EnvironmentResponse {
- $siteEnvParts = explode('.', $alias);
- [$applicationAlias, $environmentAlias] = $siteEnvParts;
- $this->logger->debug("Searching for an environment matching alias $applicationAlias.$environmentAlias.");
- $customerApplication = $this->getApplicationFromAlias($applicationAlias);
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $environmentsResource = new Environments($acquiaCloudClient);
- $environments = $environmentsResource->getAll($customerApplication->uuid);
- foreach ($environments as $environment) {
- if ($environment->name === $environmentAlias) {
- $this->logger->debug("Found environment {$environment->uuid} matching $environmentAlias.");
-
- return $environment;
- }
- }
-
- throw new AcquiaCliException("Environment not found matching the alias {alias}", ['alias' => "$applicationAlias.$environmentAlias"]);
- }
-
- private function getApplicationFromAlias(string $applicationAlias): mixed {
- return self::getAliasCache()
- ->get($applicationAlias, function () use ($applicationAlias) {
- return $this->doGetApplicationFromAlias($applicationAlias);
- });
- }
-
- /**
- * Return the ACLI alias cache.
- */
- public static function getAliasCache(): AliasCache {
- return new AliasCache('acli_aliases');
- }
-
- private function doGetApplicationFromAlias(string $applicationAlias): mixed {
- if (!strpos($applicationAlias, ':')) {
- $applicationAlias = '*:' . $applicationAlias;
- }
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- // No need to clear this query later since getClient() is a factory method.
- $acquiaCloudClient->addQuery('filter', 'hosting=@' . $applicationAlias);
- // Allow Cloud Platform users with 'support' role to resolve aliases for applications to
- // which they don't explicitly belong.
- $accountResource = new Account($acquiaCloudClient);
- $account = $accountResource->get();
- if ($account->flags->support) {
- $acquiaCloudClient->addQuery('all', 'true');
- }
- $applicationsResource = new Applications($acquiaCloudClient);
- $customerApplications = $applicationsResource->getAll();
- if (count($customerApplications) === 0) {
- throw new AcquiaCliException("No applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]);
- }
- if (count($customerApplications) > 1) {
- $callback = static function (mixed $element) {
- return $element->hosting->id;
- };
- $aliases = array_map($callback, (array) $customerApplications);
- $this->io->error(sprintf("Use a unique application alias: %s", implode(', ', $aliases)));
- throw new AcquiaCliException("Multiple applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]);
- }
-
- $customerApplication = $customerApplications[0];
-
- $this->logger->debug("Found application {$customerApplication->uuid} matching alias $applicationAlias.");
-
- return $customerApplication;
- }
-
- protected function requireCloudIdeEnvironment(): void {
- if (!self::isAcquiaCloudIde() || !self::getThisCloudIdeUuid()) {
- throw new AcquiaCliException('This command can only be run inside of an Acquia Cloud IDE');
- }
- }
-
- /**
- * @return \stdClass|null
- */
- protected function findIdeSshKeyOnCloud(string $ideLabel, string $ideUuid): ?stdClass {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $cloudKeys = $acquiaCloudClient->request('get', '/account/ssh-keys');
- $sshKeyLabel = SshKeyCommandBase::getIdeSshKeyLabel($ideLabel, $ideUuid);
- foreach ($cloudKeys as $cloudKey) {
- if ($cloudKey->label === $sshKeyLabel) {
- return $cloudKey;
- }
- }
- return NULL;
- }
-
- public function checkForNewVersion(): bool|string {
- // Input not set if called from an exception listener.
- if (!isset($this->input)) {
- return FALSE;
- }
- // Running on API commands would corrupt JSON output.
- if (str_contains($this->input->getArgument('command'), 'api:')
- || str_contains($this->input->getArgument('command'), 'acsf:')) {
- return FALSE;
- }
- // Bail in Cloud IDEs to avoid hitting GitHub API rate limits.
- if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
- return FALSE;
- }
- try {
- if ($latest = $this->hasUpdate()) {
- return $latest;
- }
- }
- catch (Exception) {
- $this->logger->debug("Could not determine if Acquia CLI has a new version available.");
- }
- return FALSE;
- }
-
- /**
- * Check if an update is available.
- *
- * @todo unify with consolidation/self-update and support unstable channels
- */
- protected function hasUpdate(): bool|string {
- $versionParser = new VersionParser();
- // Fail fast on development builds (throw UnexpectedValueException).
- $currentVersion = $versionParser->normalize($this->getApplication()->getVersion());
- $client = $this->getUpdateClient();
- $response = $client->get('https://api.github.com/repos/acquia/cli/releases');
- if ($response->getStatusCode() !== 200) {
- $this->logger->debug('Encountered ' . $response->getStatusCode() . ' error when attempting to check for new ACLI releases on GitHub: ' . $response->getReasonPhrase());
- return FALSE;
- }
-
- $releases = json_decode((string) $response->getBody(), FALSE, 512, JSON_THROW_ON_ERROR);
- if (!isset($releases[0])) {
- $this->logger->debug('No releases found at GitHub repository acquia/cli');
- return FALSE;
- }
-
- foreach ($releases as $release) {
- if (!$release->prerelease) {
- /**
- * @var string $version
- */
- $version = $release->tag_name;
- $versionStability = VersionParser::parseStability($version);
- $versionIsNewer = Comparator::greaterThan($versionParser->normalize($version), $currentVersion);
- if ($versionStability === 'stable' && $versionIsNewer) {
- return $version;
- }
- return FALSE;
- }
- }
- return FALSE;
- }
-
- public function setUpdateClient(\GuzzleHttp\Client $client): void {
- $this->updateClient = $client;
- }
-
- public function getUpdateClient(): \GuzzleHttp\Client {
- if (!isset($this->updateClient)) {
- $stack = HandlerStack::create();
- $stack->push(new CacheMiddleware(
- new PrivateCacheStrategy(
- new Psr6CacheStorage(
- new FilesystemAdapter('acli')
- )
- )
- ),
- 'cache');
- $client = new \GuzzleHttp\Client(['handler' => $stack]);
- $this->setUpdateClient($client);
- }
- return $this->updateClient;
- }
-
- protected function fillMissingRequiredApplicationUuid(InputInterface $input, OutputInterface $output): void {
- if ($input->hasArgument('applicationUuid') && !$input->getArgument('applicationUuid') && $this->getDefinition()->getArgument('applicationUuid')->isRequired()) {
- $output->writeln('Inferring Cloud Application UUID for this command since none was provided...', OutputInterface::VERBOSITY_VERBOSE);
- if ($applicationUuid = $this->determineCloudApplication()) {
- $output->writeln("Set application uuid to $applicationUuid>", OutputInterface::VERBOSITY_VERBOSE);
- $input->setArgument('applicationUuid', $applicationUuid);
- }
- }
- }
-
- private function convertUserAliasToUuid(InputInterface $input, string $userUuidArgument, string $orgUuidArgument): void {
- if ($input->hasArgument($userUuidArgument)
- && $input->getArgument($userUuidArgument)
- && $input->hasArgument($orgUuidArgument)
- && $input->getArgument($orgUuidArgument)
- ) {
- $userUuID = $input->getArgument($userUuidArgument);
- $orgUuid = $input->getArgument($orgUuidArgument);
- $userUuid = $this->validateUserUuid($userUuID, $orgUuid);
- $input->setArgument($userUuidArgument, $userUuid);
+ private function getApplicationFromAlias(string $applicationAlias): mixed
+ {
+ return self::getAliasCache()
+ ->get($applicationAlias, function () use ($applicationAlias) {
+ return $this->doGetApplicationFromAlias($applicationAlias);
+ });
+ }
+
+ /**
+ * Return the ACLI alias cache.
+ */
+ public static function getAliasCache(): AliasCache
+ {
+ return new AliasCache('acli_aliases');
+ }
+
+ private function doGetApplicationFromAlias(string $applicationAlias): mixed
+ {
+ if (!strpos($applicationAlias, ':')) {
+ $applicationAlias = '*:' . $applicationAlias;
+ }
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ // No need to clear this query later since getClient() is a factory method.
+ $acquiaCloudClient->addQuery('filter', 'hosting=@' . $applicationAlias);
+ // Allow Cloud Platform users with 'support' role to resolve aliases for applications to
+ // which they don't explicitly belong.
+ $accountResource = new Account($acquiaCloudClient);
+ $account = $accountResource->get();
+ if ($account->flags->support) {
+ $acquiaCloudClient->addQuery('all', 'true');
+ }
+ $applicationsResource = new Applications($acquiaCloudClient);
+ $customerApplications = $applicationsResource->getAll();
+ if (count($customerApplications) === 0) {
+ throw new AcquiaCliException("No applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]);
+ }
+ if (count($customerApplications) > 1) {
+ $callback = static function (mixed $element) {
+ return $element->hosting->id;
+ };
+ $aliases = array_map($callback, (array) $customerApplications);
+ $this->io->error(sprintf("Use a unique application alias: %s", implode(', ', $aliases)));
+ throw new AcquiaCliException("Multiple applications match the alias {applicationAlias}", ['applicationAlias' => $applicationAlias]);
+ }
+
+ $customerApplication = $customerApplications[0];
+
+ $this->logger->debug("Found application {$customerApplication->uuid} matching alias $applicationAlias.");
+
+ return $customerApplication;
+ }
+
+ protected function requireCloudIdeEnvironment(): void
+ {
+ if (!self::isAcquiaCloudIde() || !self::getThisCloudIdeUuid()) {
+ throw new AcquiaCliException('This command can only be run inside of an Acquia Cloud IDE');
+ }
}
- }
- /**
- * @param string $userUuidArgument User alias like uuid or email.
- * @param string $orgUuidArgument Organization uuid.
- * @return string User uuid from alias
- */
- private function validateUserUuid(string $userUuidArgument, string $orgUuidArgument): string {
- try {
- self::validateUuid($userUuidArgument);
+ /**
+ * @return \stdClass|null
+ */
+ protected function findIdeSshKeyOnCloud(string $ideLabel, string $ideUuid): ?stdClass
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $cloudKeys = $acquiaCloudClient->request('get', '/account/ssh-keys');
+ $sshKeyLabel = SshKeyCommandBase::getIdeSshKeyLabel($ideLabel, $ideUuid);
+ foreach ($cloudKeys as $cloudKey) {
+ if ($cloudKey->label === $sshKeyLabel) {
+ return $cloudKey;
+ }
+ }
+ return null;
+ }
+
+ public function checkForNewVersion(): bool|string
+ {
+ // Input not set if called from an exception listener.
+ if (!isset($this->input)) {
+ return false;
+ }
+ // Running on API commands would corrupt JSON output.
+ if (
+ str_contains($this->input->getArgument('command'), 'api:')
+ || str_contains($this->input->getArgument('command'), 'acsf:')
+ ) {
+ return false;
+ }
+ // Bail in Cloud IDEs to avoid hitting GitHub API rate limits.
+ if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
+ return false;
+ }
+ try {
+ if ($latest = $this->hasUpdate()) {
+ return $latest;
+ }
+ } catch (Exception) {
+ $this->logger->debug("Could not determine if Acquia CLI has a new version available.");
+ }
+ return false;
+ }
+
+ /**
+ * Check if an update is available.
+ *
+ * @todo unify with consolidation/self-update and support unstable channels
+ */
+ protected function hasUpdate(): bool|string
+ {
+ $versionParser = new VersionParser();
+ // Fail fast on development builds (throw UnexpectedValueException).
+ $currentVersion = $versionParser->normalize($this->getApplication()->getVersion());
+ $client = $this->getUpdateClient();
+ $response = $client->get('https://api.github.com/repos/acquia/cli/releases');
+ if ($response->getStatusCode() !== 200) {
+ $this->logger->debug('Encountered ' . $response->getStatusCode() . ' error when attempting to check for new ACLI releases on GitHub: ' . $response->getReasonPhrase());
+ return false;
+ }
+
+ $releases = json_decode((string) $response->getBody(), false, 512, JSON_THROW_ON_ERROR);
+ if (!isset($releases[0])) {
+ $this->logger->debug('No releases found at GitHub repository acquia/cli');
+ return false;
+ }
+
+ foreach ($releases as $release) {
+ if (!$release->prerelease) {
+ /**
+ * @var string $version
+ */
+ $version = $release->tag_name;
+ $versionStability = VersionParser::parseStability($version);
+ $versionIsNewer = Comparator::greaterThan($versionParser->normalize($version), $currentVersion);
+ if ($versionStability === 'stable' && $versionIsNewer) {
+ return $version;
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public function setUpdateClient(\GuzzleHttp\Client $client): void
+ {
+ $this->updateClient = $client;
+ }
+
+ public function getUpdateClient(): \GuzzleHttp\Client
+ {
+ if (!isset($this->updateClient)) {
+ $stack = HandlerStack::create();
+ $stack->push(
+ new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new Psr6CacheStorage(
+ new FilesystemAdapter('acli')
+ )
+ )
+ ),
+ 'cache'
+ );
+ $client = new \GuzzleHttp\Client(['handler' => $stack]);
+ $this->setUpdateClient($client);
+ }
+ return $this->updateClient;
+ }
+
+ protected function fillMissingRequiredApplicationUuid(InputInterface $input, OutputInterface $output): void
+ {
+ if ($input->hasArgument('applicationUuid') && !$input->getArgument('applicationUuid') && $this->getDefinition()->getArgument('applicationUuid')->isRequired()) {
+ $output->writeln('Inferring Cloud Application UUID for this command since none was provided...', OutputInterface::VERBOSITY_VERBOSE);
+ if ($applicationUuid = $this->determineCloudApplication()) {
+ $output->writeln("Set application uuid to $applicationUuid>", OutputInterface::VERBOSITY_VERBOSE);
+ $input->setArgument('applicationUuid', $applicationUuid);
+ }
+ }
+ }
+
+ private function convertUserAliasToUuid(InputInterface $input, string $userUuidArgument, string $orgUuidArgument): void
+ {
+ if (
+ $input->hasArgument($userUuidArgument)
+ && $input->getArgument($userUuidArgument)
+ && $input->hasArgument($orgUuidArgument)
+ && $input->getArgument($orgUuidArgument)
+ ) {
+ $userUuID = $input->getArgument($userUuidArgument);
+ $orgUuid = $input->getArgument($orgUuidArgument);
+ $userUuid = $this->validateUserUuid($userUuID, $orgUuid);
+ $input->setArgument($userUuidArgument, $userUuid);
+ }
+ }
+
+ /**
+ * @param string $userUuidArgument User alias like uuid or email.
+ * @param string $orgUuidArgument Organization uuid.
+ * @return string User uuid from alias
+ */
+ private function validateUserUuid(string $userUuidArgument, string $orgUuidArgument): string
+ {
+ try {
+ self::validateUuid($userUuidArgument);
+ } catch (ValidatorException) {
+ // Since this isn't a valid UUID, assuming this is email address.
+ return $this->getUserUuidFromUserAlias($userUuidArgument, $orgUuidArgument);
+ }
+
+ return $userUuidArgument;
+ }
+
+ private static function getNotificationUuid(string $notification): string
+ {
+ // Greedily hope this is already a UUID.
+ try {
+ self::validateUuid($notification);
+ return $notification;
+ } catch (ValidatorException) {
+ }
+
+ // Not a UUID, maybe a JSON object?
+ try {
+ $json = json_decode($notification, null, 4, JSON_THROW_ON_ERROR);
+ if (is_object($json)) {
+ return self::getNotificationUuidFromResponse($json);
+ }
+ if (is_string($json)) {
+ // In rare cases, JSON can decode to a string that's a valid UUID.
+ self::validateUuid($json);
+ return $json;
+ }
+ } catch (JsonException | AcquiaCliException | ValidatorException) {
+ }
+
+ // Last chance, maybe a URL?
+ try {
+ return self::getNotificationUuidFromUrl($notification);
+ } catch (ValidatorException | AcquiaCliException) {
+ }
+
+ // Womp womp.
+ throw new AcquiaCliException('Notification format is not one of UUID, JSON response, or URL');
+ }
+
+ /**
+ * @param String $userAlias User alias like uuid or email.
+ * @param String $orgUuidArgument Organization uuid.
+ * @return string User uuid from alias
+ */
+ private function getUserUuidFromUserAlias(string $userAlias, string $orgUuidArgument): string
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $organizationResource = new Organizations($acquiaCloudClient);
+ $orgMembers = $organizationResource->getMembers($orgUuidArgument);
+
+ // If there are no members.
+ if (count($orgMembers) === 0) {
+ throw new AcquiaCliException('Organization has no members');
+ }
+
+ foreach ($orgMembers as $member) {
+ // If email matches with any member.
+ if ($member->mail === $userAlias) {
+ return $member->uuid;
+ }
+ }
+
+ throw new AcquiaCliException('No matching user found in this organization');
}
- catch (ValidatorException) {
- // Since this isn't a valid UUID, assuming this is email address.
- return $this->getUserUuidFromUserAlias($userUuidArgument, $orgUuidArgument);
+
+ protected function convertApplicationAliasToUuid(InputInterface $input): void
+ {
+ if ($input->hasArgument('applicationUuid') && $input->getArgument('applicationUuid')) {
+ $applicationUuidArgument = $input->getArgument('applicationUuid');
+ $applicationUuid = $this->validateApplicationUuid($applicationUuidArgument);
+ $input->setArgument('applicationUuid', $applicationUuid);
+ }
}
- return $userUuidArgument;
- }
+ protected function convertEnvironmentAliasToUuid(InputInterface $input, string $argumentName): void
+ {
+ if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) {
+ $envUuidArgument = $input->getArgument($argumentName);
+ $environmentUuid = $this->validateEnvironmentUuid($envUuidArgument, $argumentName);
+ $input->setArgument($argumentName, $environmentUuid);
+ }
+ }
- private static function getNotificationUuid(string $notification): string {
- // Greedily hope this is already a UUID.
- try {
- self::validateUuid($notification);
- return $notification;
+ protected function convertNotificationToUuid(InputInterface $input, string $argumentName): void
+ {
+ if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) {
+ $notificationArgument = $input->getArgument($argumentName);
+ $notificationUuid = CommandBase::getNotificationUuid($notificationArgument);
+ $input->setArgument($argumentName, $notificationUuid);
+ }
}
- catch (ValidatorException) {
+
+ public static function getSitegroup(EnvironmentResponse $environment): string
+ {
+ $sshUrlParts = explode('.', $environment->sshUrl);
+ return reset($sshUrlParts);
}
- // Not a UUID, maybe a JSON object?
- try {
- $json = json_decode($notification, NULL, 4, JSON_THROW_ON_ERROR);
- if (is_object($json)) {
- return self::getNotificationUuidFromResponse($json);
- }
- if (is_string($json)) {
- // In rare cases, JSON can decode to a string that's a valid UUID.
- self::validateUuid($json);
- return $json;
- }
+ public static function getEnvironmentAlias(EnvironmentResponse $environment): string
+ {
+ $sshUrlParts = explode('@', $environment->sshUrl);
+ return reset($sshUrlParts);
}
- catch (JsonException | AcquiaCliException | ValidatorException) {
+
+ protected function isAcsfEnv(mixed $cloudEnvironment): bool
+ {
+ if (str_contains($cloudEnvironment->sshUrl, 'enterprise-g1')) {
+ return true;
+ }
+ foreach ($cloudEnvironment->domains as $domain) {
+ if (str_contains($domain, 'acsitefactory')) {
+ return true;
+ }
+ }
+
+ return false;
}
- // Last chance, maybe a URL?
- try {
- return self::getNotificationUuidFromUrl($notification);
- }
- catch (ValidatorException | AcquiaCliException) {
- }
-
- // Womp womp.
- throw new AcquiaCliException('Notification format is not one of UUID, JSON response, or URL');
- }
-
- /**
- * @param String $userAlias User alias like uuid or email.
- * @param String $orgUuidArgument Organization uuid.
- * @return string User uuid from alias
- */
- private function getUserUuidFromUserAlias(string $userAlias, string $orgUuidArgument): string {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $organizationResource = new Organizations($acquiaCloudClient);
- $orgMembers = $organizationResource->getMembers($orgUuidArgument);
+ /**
+ * @return array
+ */
+ private function getAcsfSites(EnvironmentResponse $cloudEnvironment): array
+ {
+ $envAlias = self::getEnvironmentAlias($cloudEnvironment);
+ $command = ['cat', "/var/www/site-php/$envAlias/multisite-config.json"];
+ $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, false);
+ if ($process->isSuccessful()) {
+ return json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR);
+ }
+ throw new AcquiaCliException("Could not get ACSF sites");
+ }
+
+ /**
+ * @return array
+ */
+ private function getCloudSites(EnvironmentResponse $cloudEnvironment): array
+ {
+ $sitegroup = self::getSitegroup($cloudEnvironment);
+ $command = ['ls', $this->getCloudSitesPath($cloudEnvironment, $sitegroup)];
+ $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, false);
+ $sites = array_filter(explode("\n", trim($process->getOutput())));
+ if ($process->isSuccessful() && $sites) {
+ return $sites;
+ }
- // If there are no members.
- if (count($orgMembers) === 0) {
- throw new AcquiaCliException('Organization has no members');
- }
-
- foreach ($orgMembers as $member) {
- // If email matches with any member.
- if ($member->mail === $userAlias) {
- return $member->uuid;
- }
- }
-
- throw new AcquiaCliException('No matching user found in this organization');
- }
-
- protected function convertApplicationAliasToUuid(InputInterface $input): void {
- if ($input->hasArgument('applicationUuid') && $input->getArgument('applicationUuid')) {
- $applicationUuidArgument = $input->getArgument('applicationUuid');
- $applicationUuid = $this->validateApplicationUuid($applicationUuidArgument);
- $input->setArgument('applicationUuid', $applicationUuid);
- }
- }
-
- protected function convertEnvironmentAliasToUuid(InputInterface $input, string $argumentName): void {
- if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) {
- $envUuidArgument = $input->getArgument($argumentName);
- $environmentUuid = $this->validateEnvironmentUuid($envUuidArgument, $argumentName);
- $input->setArgument($argumentName, $environmentUuid);
- }
- }
-
- protected function convertNotificationToUuid(InputInterface $input, string $argumentName): void {
- if ($input->hasArgument($argumentName) && $input->getArgument($argumentName)) {
- $notificationArgument = $input->getArgument($argumentName);
- $notificationUuid = CommandBase::getNotificationUuid($notificationArgument);
- $input->setArgument($argumentName, $notificationUuid);
- }
- }
-
- public static function getSitegroup(EnvironmentResponse $environment): string {
- $sshUrlParts = explode('.', $environment->sshUrl);
- return reset($sshUrlParts);
- }
-
- public static function getEnvironmentAlias(EnvironmentResponse $environment): string {
- $sshUrlParts = explode('@', $environment->sshUrl);
- return reset($sshUrlParts);
- }
-
- protected function isAcsfEnv(mixed $cloudEnvironment): bool {
- if (str_contains($cloudEnvironment->sshUrl, 'enterprise-g1')) {
- return TRUE;
- }
- foreach ($cloudEnvironment->domains as $domain) {
- if (str_contains($domain, 'acsitefactory')) {
- return TRUE;
- }
- }
-
- return FALSE;
- }
-
- /**
- * @return array
- */
- private function getAcsfSites(EnvironmentResponse $cloudEnvironment): array {
- $envAlias = self::getEnvironmentAlias($cloudEnvironment);
- $command = ['cat', "/var/www/site-php/$envAlias/multisite-config.json"];
- $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, FALSE);
- if ($process->isSuccessful()) {
- return json_decode($process->getOutput(), TRUE, 512, JSON_THROW_ON_ERROR);
- }
- throw new AcquiaCliException("Could not get ACSF sites");
- }
-
- /**
- * @return array
- */
- private function getCloudSites(EnvironmentResponse $cloudEnvironment): array {
- $sitegroup = self::getSitegroup($cloudEnvironment);
- $command = ['ls', $this->getCloudSitesPath($cloudEnvironment, $sitegroup)];
- $process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, FALSE);
- $sites = array_filter(explode("\n", trim($process->getOutput())));
- if ($process->isSuccessful() && $sites) {
- return $sites;
- }
-
- throw new AcquiaCliException("Could not get Cloud sites for " . $cloudEnvironment->name);
- }
-
- protected function getCloudSitesPath(mixed $cloudEnvironment, mixed $sitegroup): string {
- if ($cloudEnvironment->platform === 'cloud-next') {
- $path = "/home/clouduser/{$cloudEnvironment->name}/sites";
- }
- else {
- $path = "/mnt/files/$sitegroup.{$cloudEnvironment->name}/sites";
- }
- return $path;
- }
-
- protected function promptChooseAcsfSite(EnvironmentResponse $cloudEnvironment): mixed {
- $choices = [];
- $acsfSites = $this->getAcsfSites($cloudEnvironment);
- foreach ($acsfSites['sites'] as $domain => $acsfSite) {
- $choices[] = "{$acsfSite['name']} ($domain)";
- }
- $choice = $this->io->choice('Choose a site', $choices, $choices[0]);
- $key = array_search($choice, $choices, TRUE);
- $sites = array_values($acsfSites['sites']);
- $site = $sites[$key];
-
- return $site['name'];
- }
-
- protected function promptChooseCloudSite(EnvironmentResponse $cloudEnvironment): mixed {
- $sites = $this->getCloudSites($cloudEnvironment);
- if (count($sites) === 1) {
- $site = reset($sites);
- $this->logger->debug("Only a single Cloud site was detected. Assuming site is $site");
- return $site;
- }
- $this->logger->debug("Multisite detected");
- $this->warnMultisite();
- return $this->io->choice('Choose a site', $sites, $sites[0]);
- }
-
- protected static function isLandoEnv(): bool {
- return AcquiaDrupalEnvironmentDetector::isLandoEnv();
- }
-
- protected function reAuthenticate(string $apiKey, string $apiSecret, ?string $baseUri, ?string $accountsUri): void {
- // Client service needs to be reinitialized with new credentials in case
- // this is being run as a sub-command.
- // @see https://github.com/acquia/cli/issues/403
- $this->cloudApiClientService->setConnector(new Connector([
- 'key' => $apiKey,
- 'secret' => $apiSecret,
- ], $baseUri, $accountsUri));
- }
-
- private function warnMultisite(): void {
- $this->io->note("This is a multisite application. Drupal will load the default site unless you've configured sites.php for this environment: https://docs.acquia.com/cloud-platform/develop/drupal/multisite/");
- }
-
- protected function setDirAndRequireProjectCwd(InputInterface $input): void {
- $this->determineDir($input);
- if ($this->dir !== '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
- throw new AcquiaCliException('Run this command from the {dir} directory', ['dir' => '/home/ide/project']);
- }
- }
-
- protected function determineDir(InputInterface $input): void {
- if (isset($this->dir)) {
- return;
- }
-
- if ($input->hasOption('dir') && $dir = $input->getOption('dir')) {
- $this->dir = $dir;
- }
- elseif ($this->projectDir) {
- $this->dir = $this->projectDir;
- }
- else {
- $this->dir = getcwd();
- }
- }
-
- protected function getOutputCallback(OutputInterface $output, Checklist $checklist): Closure {
- return static function (mixed $type, mixed $buffer) use ($checklist, $output): void {
- if (!$output->isVerbose() && $checklist->getItems()) {
- $checklist->updateProgressBar($buffer);
- }
- $output->writeln($buffer, OutputInterface::VERBOSITY_VERY_VERBOSE);
- };
- }
-
- protected function executeAllScripts(Closure $outputCallback, Checklist $checklist): void {
- $this->runComposerScripts($outputCallback, $checklist);
- $this->runDrushCacheClear($outputCallback, $checklist);
- $this->runDrushSqlSanitize($outputCallback, $checklist);
- }
-
- protected function runComposerScripts(callable $outputCallback, Checklist $checklist): void {
- if (!file_exists(Path::join($this->dir, 'composer.json'))) {
- $this->io->note('composer.json file not found. Skipping composer install.');
- return;
- }
- if (!$this->localMachineHelper->commandExists('composer')) {
- $this->io->note('Composer not found. Skipping composer install.');
- return;
- }
- if (file_exists(Path::join($this->dir, 'vendor'))) {
- $this->io->note('Composer dependencies already installed. Skipping composer install.');
- return;
- }
- $checklist->addItem("Installing Composer dependencies");
- $this->composerInstall($outputCallback);
- $checklist->completePreviousItem();
- }
-
- protected function runDrushCacheClear(Closure $outputCallback, Checklist $checklist): void {
- if ($this->getDrushDatabaseConnectionStatus()) {
- $checklist->addItem('Clearing Drupal caches via Drush');
- // @todo Add support for Drush 8.
- $process = $this->localMachineHelper->execute([
- 'drush',
- 'cache:rebuild',
- '--yes',
- '--no-interaction',
- '--verbose',
- ], $outputCallback, $this->dir, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to rebuild Drupal caches via Drush. {message}', ['message' => $process->getErrorOutput()]);
- }
- $checklist->completePreviousItem();
- }
- else {
- $this->logger->notice('Drush does not have an active database connection. Skipping cache:rebuild');
- }
- }
-
- protected function runDrushSqlSanitize(Closure $outputCallback, Checklist $checklist): void {
- if ($this->getDrushDatabaseConnectionStatus()) {
- $checklist->addItem('Sanitizing database via Drush');
- $process = $this->localMachineHelper->execute([
- 'drush',
- 'sql:sanitize',
- '--yes',
- '--no-interaction',
- '--verbose',
- ], $outputCallback, $this->dir, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to sanitize Drupal database via Drush. {message}', ['message' => $process->getErrorOutput()]);
- }
- $checklist->completePreviousItem();
- $this->io->newLine();
- $this->io->text('Your database was sanitized via drush sql:sanitize>. This has changed all user passwords to randomly generated strings. To log in to your Drupal site, use drush uli>');
- }
- else {
- $this->logger->notice('Drush does not have an active database connection. Skipping sql:sanitize.');
- }
- }
-
- private function composerInstall(?callable $outputCallback): void {
- $process = $this->localMachineHelper->execute([
- 'composer',
- 'install',
- '--no-interaction',
- ], $outputCallback, $this->dir, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to install Drupal dependencies via Composer. {message}',
- ['message' => $process->getErrorOutput()]);
- }
- }
-
- protected function getDrushDatabaseConnectionStatus(Closure $outputCallback = NULL): bool {
- if (isset($this->drushHasActiveDatabaseConnection)) {
- return $this->drushHasActiveDatabaseConnection;
- }
- if ($this->localMachineHelper->commandExists('drush')) {
- $process = $this->localMachineHelper->execute([
- 'drush',
- 'status',
- '--fields=db-status,drush-version',
- '--format=json',
+ throw new AcquiaCliException("Could not get Cloud sites for " . $cloudEnvironment->name);
+ }
+
+ protected function getCloudSitesPath(mixed $cloudEnvironment, mixed $sitegroup): string
+ {
+ if ($cloudEnvironment->platform === 'cloud-next') {
+ $path = "/home/clouduser/{$cloudEnvironment->name}/sites";
+ } else {
+ $path = "/mnt/files/$sitegroup.{$cloudEnvironment->name}/sites";
+ }
+ return $path;
+ }
+
+ protected function promptChooseAcsfSite(EnvironmentResponse $cloudEnvironment): mixed
+ {
+ $choices = [];
+ $acsfSites = $this->getAcsfSites($cloudEnvironment);
+ foreach ($acsfSites['sites'] as $domain => $acsfSite) {
+ $choices[] = "{$acsfSite['name']} ($domain)";
+ }
+ $choice = $this->io->choice('Choose a site', $choices, $choices[0]);
+ $key = array_search($choice, $choices, true);
+ $sites = array_values($acsfSites['sites']);
+ $site = $sites[$key];
+
+ return $site['name'];
+ }
+
+ protected function promptChooseCloudSite(EnvironmentResponse $cloudEnvironment): mixed
+ {
+ $sites = $this->getCloudSites($cloudEnvironment);
+ if (count($sites) === 1) {
+ $site = reset($sites);
+ $this->logger->debug("Only a single Cloud site was detected. Assuming site is $site");
+ return $site;
+ }
+ $this->logger->debug("Multisite detected");
+ $this->warnMultisite();
+ return $this->io->choice('Choose a site', $sites, $sites[0]);
+ }
+
+ protected static function isLandoEnv(): bool
+ {
+ return AcquiaDrupalEnvironmentDetector::isLandoEnv();
+ }
+
+ protected function reAuthenticate(string $apiKey, string $apiSecret, ?string $baseUri, ?string $accountsUri): void
+ {
+ // Client service needs to be reinitialized with new credentials in case
+ // this is being run as a sub-command.
+ // @see https://github.com/acquia/cli/issues/403
+ $this->cloudApiClientService->setConnector(new Connector([
+ 'key' => $apiKey,
+ 'secret' => $apiSecret,
+ ], $baseUri, $accountsUri));
+ }
+
+ private function warnMultisite(): void
+ {
+ $this->io->note("This is a multisite application. Drupal will load the default site unless you've configured sites.php for this environment: https://docs.acquia.com/cloud-platform/develop/drupal/multisite/");
+ }
+
+ protected function setDirAndRequireProjectCwd(InputInterface $input): void
+ {
+ $this->determineDir($input);
+ if ($this->dir !== '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
+ throw new AcquiaCliException('Run this command from the {dir} directory', ['dir' => '/home/ide/project']);
+ }
+ }
+
+ protected function determineDir(InputInterface $input): void
+ {
+ if (isset($this->dir)) {
+ return;
+ }
+
+ if ($input->hasOption('dir') && $dir = $input->getOption('dir')) {
+ $this->dir = $dir;
+ } elseif ($this->projectDir) {
+ $this->dir = $this->projectDir;
+ } else {
+ $this->dir = getcwd();
+ }
+ }
+
+ protected function getOutputCallback(OutputInterface $output, Checklist $checklist): Closure
+ {
+ return static function (mixed $type, mixed $buffer) use ($checklist, $output): void {
+ if (!$output->isVerbose() && $checklist->getItems()) {
+ $checklist->updateProgressBar($buffer);
+ }
+ $output->writeln($buffer, OutputInterface::VERBOSITY_VERY_VERBOSE);
+ };
+ }
+
+ protected function executeAllScripts(Closure $outputCallback, Checklist $checklist): void
+ {
+ $this->runComposerScripts($outputCallback, $checklist);
+ $this->runDrushCacheClear($outputCallback, $checklist);
+ $this->runDrushSqlSanitize($outputCallback, $checklist);
+ }
+
+ protected function runComposerScripts(callable $outputCallback, Checklist $checklist): void
+ {
+ if (!file_exists(Path::join($this->dir, 'composer.json'))) {
+ $this->io->note('composer.json file not found. Skipping composer install.');
+ return;
+ }
+ if (!$this->localMachineHelper->commandExists('composer')) {
+ $this->io->note('Composer not found. Skipping composer install.');
+ return;
+ }
+ if (file_exists(Path::join($this->dir, 'vendor'))) {
+ $this->io->note('Composer dependencies already installed. Skipping composer install.');
+ return;
+ }
+ $checklist->addItem("Installing Composer dependencies");
+ $this->composerInstall($outputCallback);
+ $checklist->completePreviousItem();
+ }
+
+ protected function runDrushCacheClear(Closure $outputCallback, Checklist $checklist): void
+ {
+ if ($this->getDrushDatabaseConnectionStatus()) {
+ $checklist->addItem('Clearing Drupal caches via Drush');
+ // @todo Add support for Drush 8.
+ $process = $this->localMachineHelper->execute([
+ 'drush',
+ 'cache:rebuild',
+ '--yes',
+ '--no-interaction',
+ '--verbose',
+ ], $outputCallback, $this->dir, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to rebuild Drupal caches via Drush. {message}', ['message' => $process->getErrorOutput()]);
+ }
+ $checklist->completePreviousItem();
+ } else {
+ $this->logger->notice('Drush does not have an active database connection. Skipping cache:rebuild');
+ }
+ }
+
+ protected function runDrushSqlSanitize(Closure $outputCallback, Checklist $checklist): void
+ {
+ if ($this->getDrushDatabaseConnectionStatus()) {
+ $checklist->addItem('Sanitizing database via Drush');
+ $process = $this->localMachineHelper->execute([
+ 'drush',
+ 'sql:sanitize',
+ '--yes',
+ '--no-interaction',
+ '--verbose',
+ ], $outputCallback, $this->dir, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to sanitize Drupal database via Drush. {message}', ['message' => $process->getErrorOutput()]);
+ }
+ $checklist->completePreviousItem();
+ $this->io->newLine();
+ $this->io->text('Your database was sanitized via drush sql:sanitize>. This has changed all user passwords to randomly generated strings. To log in to your Drupal site, use drush uli>');
+ } else {
+ $this->logger->notice('Drush does not have an active database connection. Skipping sql:sanitize.');
+ }
+ }
+
+ private function composerInstall(?callable $outputCallback): void
+ {
+ $process = $this->localMachineHelper->execute([
+ 'composer',
+ 'install',
'--no-interaction',
- ], $outputCallback, $this->dir, FALSE);
- if ($process->isSuccessful()) {
- $drushStatusReturnOutput = json_decode($process->getOutput(), TRUE);
- if (is_array($drushStatusReturnOutput) && array_key_exists('db-status', $drushStatusReturnOutput) && $drushStatusReturnOutput['db-status'] === 'Connected') {
- $this->drushHasActiveDatabaseConnection = TRUE;
- return $this->drushHasActiveDatabaseConnection;
- }
- }
- }
-
- $this->drushHasActiveDatabaseConnection = FALSE;
-
- return $this->drushHasActiveDatabaseConnection;
- }
-
- protected function createMySqlDumpOnLocal(string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = NULL): string {
- $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']);
- $filename = "acli-mysql-dump-{$dbName}.sql.gz";
- $localTempDir = sys_get_temp_dir();
- $localFilepath = $localTempDir . '/' . $filename;
- $this->logger->debug("Dumping MySQL database to $localFilepath on this machine");
- $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']);
- if ($outputCallback) {
- $outputCallback('out', "Dumping MySQL database to $localFilepath on this machine");
- }
- if ($this->localMachineHelper->commandExists('pv')) {
- $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | pv --rate --bytes | gzip -9 > $localFilepath";
- }
- else {
- $this->io->warning('Install `pv` to see progress bar');
- $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | gzip -9 > $localFilepath";
- }
-
- $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful() || $process->getOutput()) {
- throw new AcquiaCliException('Unable to create a dump of the local database. {message}', ['message' => $process->getErrorOutput()]);
- }
-
- return $localFilepath;
- }
-
- /** @infection-ignore-all */
- protected function promptOpenBrowserToCreateToken(
- InputInterface $input
- ): void {
- if (!$input->getOption('key') || !$input->getOption('secret')) {
- $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens';
- $this->output->writeln("You will need a Cloud Platform API token from $tokenUrl>");
-
- if (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && $this->io->confirm('Do you want to open this page to generate a token now?')) {
- $this->localMachineHelper->startBrowser($tokenUrl);
- }
- }
- }
-
- protected function determineApiKey(): string {
- return $this->determineOption('key', FALSE, $this->validateApiKey(...));
- }
-
- private function validateApiKey(mixed $key): string {
- $violations = Validation::createValidator()->validate($key, [
- new Length(['min' => 10]),
- new NotBlank(),
- new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
- }
- return $key;
- }
-
- protected function determineApiSecret(): string {
- return $this->determineOption('secret', TRUE, $this->validateApiKey(...));
- }
-
- /**
- * Get an option, either passed explicitly or via interactive prompt.
- *
- * Default can be passed explicitly, separately from the option default,
- * because Symfony does not make a distinction between an option value set
- * explicitly or by default. In other words, we can't prompt for the value of
- * an option that already has a default value.
- */
- protected function determineOption(string $optionName, bool $hidden = FALSE, ?Closure $validator = NULL, ?Closure $normalizer = NULL, string|bool|null $default = NULL): string|int|bool|null {
- if ($optionValue = $this->input->getOption($optionName)) {
- if (isset($normalizer)) {
- $optionValue = $normalizer($optionValue);
- }
- if (isset($validator)) {
- $validator($optionValue);
- }
- return $optionValue;
- }
- $option = $this->getDefinition()->getOption($optionName);
- if ($option->isNegatable() && $this->input->getOption("no-$optionName")) {
- return FALSE;
- }
- $optionShortcut = $option->getShortcut();
- $description = lcfirst($option->getDescription());
- if ($optionShortcut) {
- $optionString = "option -$optionShortcut>, --$optionName>";
- }
- else {
- $optionString = "option --$optionName>";
- }
- if ($option->acceptValue()) {
- $message = "Enter $description ($optionString)";
- }
- else {
- $message = "Do you want to $description ($optionString)?";
- }
- $optional = $option->isValueOptional();
- $message .= $optional ? ' (optional)' : '';
- $message .= $hidden ? ' (input will be hidden)' : '';
- if ($option->acceptValue()) {
- $question = new Question($message, $default);
- }
- else {
- $question = new ConfirmationQuestion($message, $default);
- }
- $question->setHidden($this->localMachineHelper->useTty() && $hidden);
- $question->setHiddenFallback($hidden);
- if (isset($normalizer)) {
- $question->setNormalizer($normalizer);
- }
- if (isset($validator)) {
- $question->setValidator($validator);
- }
- $optionValue = $this->io->askQuestion($question);
- // Question bypasses validation if session is non-interactive.
- if (!$optional && is_null($optionValue)) {
- throw new AcquiaCliException($message);
- }
- return $optionValue;
- }
-
- /**
- * Get the first environment for a given Cloud application matching a filter.
- */
- private function getAnyAhEnvironment(string $cloudAppUuid, callable $filter): EnvironmentResponse|false {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $environmentResource = new Environments($acquiaCloudClient);
- /** @var EnvironmentResponse[] $applicationEnvironments */
- $applicationEnvironments = iterator_to_array($environmentResource->getAll($cloudAppUuid));
- $candidates = array_filter($applicationEnvironments, $filter);
- return reset($candidates);
- }
-
- /**
- * Get the first non-prod environment for a given Cloud application.
- */
- protected function getAnyNonProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false {
- return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) {
- return !$environment->flags->production && $environment->type === 'drupal';
- });
- }
-
- /**
- * Get the first prod environment for a given Cloud application.
- */
- protected function getAnyProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false {
- return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) {
- return $environment->flags->production && $environment->type === 'drupal';
- });
- }
-
- /**
- * Get the first VCS URL for a given Cloud application.
- */
- protected function getAnyVcsUrl(string $cloudAppUuid): string {
- $environment = $this->getAnyAhEnvironment($cloudAppUuid, function (): bool {
- return TRUE;
- });
- return $environment->vcs->url;
- }
-
- protected function validateApplicationUuid(string $applicationUuidArgument): mixed {
- try {
- self::validateUuid($applicationUuidArgument);
- }
- catch (ValidatorException) {
- // Since this isn't a valid UUID, let's see if it's a valid alias.
- $alias = $this->normalizeAlias($applicationUuidArgument);
- return $this->getApplicationFromAlias($alias)->uuid;
- }
- return $applicationUuidArgument;
- }
-
- protected function validateEnvironmentUuid(mixed $envUuidArgument, mixed $argumentName): string {
- if (is_null($envUuidArgument)) {
- throw new AcquiaCliException("{{$argumentName}} must not be null");
- }
- try {
- // Environment IDs take the form of [env-num]-[app-uuid].
- $uuidParts = explode('-', $envUuidArgument);
- unset($uuidParts[0]);
- $applicationUuid = implode('-', $uuidParts);
- self::validateUuid($applicationUuid);
- }
- catch (ValidatorException) {
- try {
- // Since this isn't a valid environment ID, let's see if it's a valid alias.
- $alias = $envUuidArgument;
- $alias = $this->normalizeAlias($alias);
- $alias = self::validateEnvironmentAlias($alias);
- return $this->getEnvironmentFromAliasArg($alias)->uuid;
- }
- catch (AcquiaCliException) {
- throw new AcquiaCliException("{{$argumentName}} must be a valid UUID or site alias.");
- }
- }
- return $envUuidArgument;
- }
-
- protected function checkAuthentication(): void {
- if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) {
- if ($this->cloudApiClientService instanceof AcsfClientService) {
- throw new AcquiaCliException('This machine is not yet authenticated with Site Factory.');
- }
- throw new AcquiaCliException('This machine is not yet authenticated with the Cloud Platform.');
- }
- }
-
- protected function waitForNotificationToComplete(Client $acquiaCloudClient, string $uuid, string $message, callable $success = NULL): bool {
- $notificationsResource = new Notifications($acquiaCloudClient);
- $notification = NULL;
- $checkNotificationStatus = static function () use ($notificationsResource, &$notification, $uuid): bool {
- $notification = $notificationsResource->get($uuid);
- // @infection-ignore-all
- return $notification->status !== 'in-progress';
- };
- if ($success === NULL) {
- $success = function () use (&$notification): void {
- $this->writeCompletedMessage($notification);
- };
- }
- LoopHelper::getLoopy($this->output, $this->io, $message, $checkNotificationStatus, $success);
- return $notification->status === 'completed';
- }
-
- private function writeCompletedMessage(NotificationResponse $notification): void {
- if ($notification->status === 'completed') {
- $this->io->success("The task with notification uuid {$notification->uuid} completed");
- }
- else if ($notification->status === 'failed') {
- $this->io->error("The task with notification uuid {$notification->uuid} failed");
- }
- else {
- throw new AcquiaCliException("Unknown task status: {$notification->status}");
- }
- $duration = strtotime($notification->completed_at) - strtotime($notification->created_at);
- $completedAt = date("D M j G:i:s T Y", strtotime($notification->completed_at));
- $this->io->writeln("Progress: {$notification->progress}");
- $this->io->writeln("Completed: $completedAt");
- $this->io->writeln("Task type: {$notification->label}");
- $this->io->writeln("Duration: $duration seconds");
- }
-
- protected static function getNotificationUuidFromResponse(object $response): string {
- if (property_exists($response, 'links')) {
- $links = $response->links;
- }
- elseif (property_exists($response, '_links')) {
- $links = $response->_links;
- }
- else {
- throw new AcquiaCliException('JSON object must contain the _links.notification.href property');
- }
- if (property_exists($links, 'notification') && property_exists($links->notification, 'href')) {
- return self::getNotificationUuidFromUrl($links->notification->href);
- }
- throw new AcquiaCliException('JSON object must contain the _links.notification.href property');
- }
-
- private static function getNotificationUuidFromUrl(string $notificationUrl): string {
- $notificationUrlPattern = '/^https:\/\/cloud.acquia.com\/api\/notifications\/([\w-]*)$/';
- if (preg_match($notificationUrlPattern, $notificationUrl, $matches)) {
- self::validateUuid($matches[1]);
- return $matches[1];
- }
- throw new AcquiaCliException('Notification UUID not found in URL');
- }
-
- protected function validateRequiredCloudPermissions(Client $acquiaCloudClient, ?string $cloudApplicationUuid, AccountResponse $account, array $requiredPermissions): void {
- $permissions = $acquiaCloudClient->request('get', "/applications/{$cloudApplicationUuid}/permissions");
- $keyedPermissions = [];
- foreach ($permissions as $permission) {
- $keyedPermissions[$permission->name] = $permission;
- }
- foreach ($requiredPermissions as $name) {
- if (!array_key_exists($name, $keyedPermissions)) {
- throw new AcquiaCliException("The Acquia Cloud Platform account {account} does not have the required '{name}' permission. Add the permissions to this user or use an API Token belonging to a different Acquia Cloud Platform user.", [
- 'account' => $account->mail,
- 'name' => $name,
+ ], $outputCallback, $this->dir, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException(
+ 'Unable to install Drupal dependencies via Composer. {message}',
+ ['message' => $process->getErrorOutput()]
+ );
+ }
+ }
+
+ protected function getDrushDatabaseConnectionStatus(Closure $outputCallback = null): bool
+ {
+ if (isset($this->drushHasActiveDatabaseConnection)) {
+ return $this->drushHasActiveDatabaseConnection;
+ }
+ if ($this->localMachineHelper->commandExists('drush')) {
+ $process = $this->localMachineHelper->execute([
+ 'drush',
+ 'status',
+ '--fields=db-status,drush-version',
+ '--format=json',
+ '--no-interaction',
+ ], $outputCallback, $this->dir, false);
+ if ($process->isSuccessful()) {
+ $drushStatusReturnOutput = json_decode($process->getOutput(), true);
+ if (is_array($drushStatusReturnOutput) && array_key_exists('db-status', $drushStatusReturnOutput) && $drushStatusReturnOutput['db-status'] === 'Connected') {
+ $this->drushHasActiveDatabaseConnection = true;
+ return $this->drushHasActiveDatabaseConnection;
+ }
+ }
+ }
+
+ $this->drushHasActiveDatabaseConnection = false;
+
+ return $this->drushHasActiveDatabaseConnection;
+ }
+
+ protected function createMySqlDumpOnLocal(string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = null): string
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']);
+ $filename = "acli-mysql-dump-{$dbName}.sql.gz";
+ $localTempDir = sys_get_temp_dir();
+ $localFilepath = $localTempDir . '/' . $filename;
+ $this->logger->debug("Dumping MySQL database to $localFilepath on this machine");
+ $this->localMachineHelper->checkRequiredBinariesExist(['mysqldump', 'gzip']);
+ if ($outputCallback) {
+ $outputCallback('out', "Dumping MySQL database to $localFilepath on this machine");
+ }
+ if ($this->localMachineHelper->commandExists('pv')) {
+ $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | pv --rate --bytes | gzip -9 > $localFilepath";
+ } else {
+ $this->io->warning('Install `pv` to see progress bar');
+ $command = "MYSQL_PWD={$dbPassword} mysqldump --host={$dbHost} --user={$dbUser} {$dbName} | gzip -9 > $localFilepath";
+ }
+
+ $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful() || $process->getOutput()) {
+ throw new AcquiaCliException('Unable to create a dump of the local database. {message}', ['message' => $process->getErrorOutput()]);
+ }
+
+ return $localFilepath;
+ }
+
+ /** @infection-ignore-all */
+ protected function promptOpenBrowserToCreateToken(
+ InputInterface $input
+ ): void {
+ if (!$input->getOption('key') || !$input->getOption('secret')) {
+ $tokenUrl = 'https://cloud.acquia.com/a/profile/tokens';
+ $this->output->writeln("You will need a Cloud Platform API token from $tokenUrl>");
+
+ if (!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && $this->io->confirm('Do you want to open this page to generate a token now?')) {
+ $this->localMachineHelper->startBrowser($tokenUrl);
+ }
+ }
+ }
+
+ protected function determineApiKey(): string
+ {
+ return $this->determineOption('key', false, $this->validateApiKey(...));
+ }
+
+ private function validateApiKey(mixed $key): string
+ {
+ $violations = Validation::createValidator()->validate($key, [
+ new Length(['min' => 10]),
+ new NotBlank(),
+ new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']),
]);
- }
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ return $key;
+ }
+
+ protected function determineApiSecret(): string
+ {
+ return $this->determineOption('secret', true, $this->validateApiKey(...));
+ }
+
+ /**
+ * Get an option, either passed explicitly or via interactive prompt.
+ *
+ * Default can be passed explicitly, separately from the option default,
+ * because Symfony does not make a distinction between an option value set
+ * explicitly or by default. In other words, we can't prompt for the value of
+ * an option that already has a default value.
+ */
+ protected function determineOption(string $optionName, bool $hidden = false, ?Closure $validator = null, ?Closure $normalizer = null, string|bool|null $default = null): string|int|bool|null
+ {
+ if ($optionValue = $this->input->getOption($optionName)) {
+ if (isset($normalizer)) {
+ $optionValue = $normalizer($optionValue);
+ }
+ if (isset($validator)) {
+ $validator($optionValue);
+ }
+ return $optionValue;
+ }
+ $option = $this->getDefinition()->getOption($optionName);
+ if ($option->isNegatable() && $this->input->getOption("no-$optionName")) {
+ return false;
+ }
+ $optionShortcut = $option->getShortcut();
+ $description = lcfirst($option->getDescription());
+ if ($optionShortcut) {
+ $optionString = "option -$optionShortcut>, --$optionName>";
+ } else {
+ $optionString = "option --$optionName>";
+ }
+ if ($option->acceptValue()) {
+ $message = "Enter $description ($optionString)";
+ } else {
+ $message = "Do you want to $description ($optionString)?";
+ }
+ $optional = $option->isValueOptional();
+ $message .= $optional ? ' (optional)' : '';
+ $message .= $hidden ? ' (input will be hidden)' : '';
+ if ($option->acceptValue()) {
+ $question = new Question($message, $default);
+ } else {
+ $question = new ConfirmationQuestion($message, $default);
+ }
+ $question->setHidden($this->localMachineHelper->useTty() && $hidden);
+ $question->setHiddenFallback($hidden);
+ if (isset($normalizer)) {
+ $question->setNormalizer($normalizer);
+ }
+ if (isset($validator)) {
+ $question->setValidator($validator);
+ }
+ $optionValue = $this->io->askQuestion($question);
+ // Question bypasses validation if session is non-interactive.
+ if (!$optional && is_null($optionValue)) {
+ throw new AcquiaCliException($message);
+ }
+ return $optionValue;
+ }
+
+ /**
+ * Get the first environment for a given Cloud application matching a filter.
+ */
+ private function getAnyAhEnvironment(string $cloudAppUuid, callable $filter): EnvironmentResponse|false
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $environmentResource = new Environments($acquiaCloudClient);
+ /** @var EnvironmentResponse[] $applicationEnvironments */
+ $applicationEnvironments = iterator_to_array($environmentResource->getAll($cloudAppUuid));
+ $candidates = array_filter($applicationEnvironments, $filter);
+ return reset($candidates);
+ }
+
+ /**
+ * Get the first non-prod environment for a given Cloud application.
+ */
+ protected function getAnyNonProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false
+ {
+ return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) {
+ return !$environment->flags->production && $environment->type === 'drupal';
+ });
+ }
+
+ /**
+ * Get the first prod environment for a given Cloud application.
+ */
+ protected function getAnyProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false
+ {
+ return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) {
+ return $environment->flags->production && $environment->type === 'drupal';
+ });
+ }
+
+ /**
+ * Get the first VCS URL for a given Cloud application.
+ */
+ protected function getAnyVcsUrl(string $cloudAppUuid): string
+ {
+ $environment = $this->getAnyAhEnvironment($cloudAppUuid, function (): bool {
+ return true;
+ });
+ return $environment->vcs->url;
+ }
+
+ protected function validateApplicationUuid(string $applicationUuidArgument): mixed
+ {
+ try {
+ self::validateUuid($applicationUuidArgument);
+ } catch (ValidatorException) {
+ // Since this isn't a valid UUID, let's see if it's a valid alias.
+ $alias = $this->normalizeAlias($applicationUuidArgument);
+ return $this->getApplicationFromAlias($alias)->uuid;
+ }
+ return $applicationUuidArgument;
+ }
+
+ protected function validateEnvironmentUuid(mixed $envUuidArgument, mixed $argumentName): string
+ {
+ if (is_null($envUuidArgument)) {
+ throw new AcquiaCliException("{{$argumentName}} must not be null");
+ }
+ try {
+ // Environment IDs take the form of [env-num]-[app-uuid].
+ $uuidParts = explode('-', $envUuidArgument);
+ unset($uuidParts[0]);
+ $applicationUuid = implode('-', $uuidParts);
+ self::validateUuid($applicationUuid);
+ } catch (ValidatorException) {
+ try {
+ // Since this isn't a valid environment ID, let's see if it's a valid alias.
+ $alias = $envUuidArgument;
+ $alias = $this->normalizeAlias($alias);
+ $alias = self::validateEnvironmentAlias($alias);
+ return $this->getEnvironmentFromAliasArg($alias)->uuid;
+ } catch (AcquiaCliException) {
+ throw new AcquiaCliException("{{$argumentName}} must be a valid UUID or site alias.");
+ }
+ }
+ return $envUuidArgument;
}
- }
- protected function validatePhpVersion(string $version): string {
- $violations = Validation::createValidator()->validate($version, [
- new Length(['min' => 3]),
- new NotBlank(),
- new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']),
- new Regex(['pattern' => '/[0-9]{1}\.[0-9]{1}/', 'message' => 'The value must be in the format "x.y"']),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+ protected function checkAuthentication(): void
+ {
+ if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) {
+ if ($this->cloudApiClientService instanceof AcsfClientService) {
+ throw new AcquiaCliException('This machine is not yet authenticated with Site Factory.');
+ }
+ throw new AcquiaCliException('This machine is not yet authenticated with the Cloud Platform.');
+ }
}
- return $version;
- }
+ protected function waitForNotificationToComplete(Client $acquiaCloudClient, string $uuid, string $message, callable $success = null): bool
+ {
+ $notificationsResource = new Notifications($acquiaCloudClient);
+ $notification = null;
+ $checkNotificationStatus = static function () use ($notificationsResource, &$notification, $uuid): bool {
+ $notification = $notificationsResource->get($uuid);
+ // @infection-ignore-all
+ return $notification->status !== 'in-progress';
+ };
+ if ($success === null) {
+ $success = function () use (&$notification): void {
+ $this->writeCompletedMessage($notification);
+ };
+ }
+ LoopHelper::getLoopy($this->output, $this->io, $message, $checkNotificationStatus, $success);
+ return $notification->status === 'completed';
+ }
+
+ private function writeCompletedMessage(NotificationResponse $notification): void
+ {
+ if ($notification->status === 'completed') {
+ $this->io->success("The task with notification uuid {$notification->uuid} completed");
+ } elseif ($notification->status === 'failed') {
+ $this->io->error("The task with notification uuid {$notification->uuid} failed");
+ } else {
+ throw new AcquiaCliException("Unknown task status: {$notification->status}");
+ }
+ $duration = strtotime($notification->completed_at) - strtotime($notification->created_at);
+ $completedAt = date("D M j G:i:s T Y", strtotime($notification->completed_at));
+ $this->io->writeln("Progress: {$notification->progress}");
+ $this->io->writeln("Completed: $completedAt");
+ $this->io->writeln("Task type: {$notification->label}");
+ $this->io->writeln("Duration: $duration seconds");
+ }
+
+ protected static function getNotificationUuidFromResponse(object $response): string
+ {
+ if (property_exists($response, 'links')) {
+ $links = $response->links;
+ } elseif (property_exists($response, '_links')) {
+ $links = $response->_links;
+ } else {
+ throw new AcquiaCliException('JSON object must contain the _links.notification.href property');
+ }
+ if (property_exists($links, 'notification') && property_exists($links->notification, 'href')) {
+ return self::getNotificationUuidFromUrl($links->notification->href);
+ }
+ throw new AcquiaCliException('JSON object must contain the _links.notification.href property');
+ }
- protected function promptChooseDrupalSite(EnvironmentResponse $environment): string {
- if ($this->isAcsfEnv($environment)) {
- return $this->promptChooseAcsfSite($environment);
+ private static function getNotificationUuidFromUrl(string $notificationUrl): string
+ {
+ $notificationUrlPattern = '/^https:\/\/cloud.acquia.com\/api\/notifications\/([\w-]*)$/';
+ if (preg_match($notificationUrlPattern, $notificationUrl, $matches)) {
+ self::validateUuid($matches[1]);
+ return $matches[1];
+ }
+ throw new AcquiaCliException('Notification UUID not found in URL');
}
- return $this->promptChooseCloudSite($environment);
- }
+ protected function validateRequiredCloudPermissions(Client $acquiaCloudClient, ?string $cloudApplicationUuid, AccountResponse $account, array $requiredPermissions): void
+ {
+ $permissions = $acquiaCloudClient->request('get', "/applications/{$cloudApplicationUuid}/permissions");
+ $keyedPermissions = [];
+ foreach ($permissions as $permission) {
+ $keyedPermissions[$permission->name] = $permission;
+ }
+ foreach ($requiredPermissions as $name) {
+ if (!array_key_exists($name, $keyedPermissions)) {
+ throw new AcquiaCliException("The Acquia Cloud Platform account {account} does not have the required '{name}' permission. Add the permissions to this user or use an API Token belonging to a different Acquia Cloud Platform user.", [
+ 'account' => $account->mail,
+ 'name' => $name,
+ ]);
+ }
+ }
+ }
+
+ protected function validatePhpVersion(string $version): string
+ {
+ $violations = Validation::createValidator()->validate($version, [
+ new Length(['min' => 3]),
+ new NotBlank(),
+ new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']),
+ new Regex(['pattern' => '/[0-9]{1}\.[0-9]{1}/', 'message' => 'The value must be in the format "x.y"']),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ return $version;
+ }
+
+ protected function promptChooseDrupalSite(EnvironmentResponse $environment): string
+ {
+ if ($this->isAcsfEnv($environment)) {
+ return $this->promptChooseAcsfSite($environment);
+ }
+
+ return $this->promptChooseCloudSite($environment);
+ }
}
diff --git a/src/Command/DocsCommand.php b/src/Command/DocsCommand.php
index ab9f1fb74..4512bafac 100644
--- a/src/Command/DocsCommand.php
+++ b/src/Command/DocsCommand.php
@@ -1,6 +1,6 @@
addArgument('product', InputArgument::OPTIONAL, 'Acquia Product Name')
- ->addUsage('acli');
- }
+final class DocsCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('product', InputArgument::OPTIONAL, 'Acquia Product Name')
+ ->addUsage('acli');
+ }
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaProducts = [
- 'Acquia CLI' => [
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaProducts = [
+ 'Acquia CLI' => [
'alias' => ['cli', 'acli'],
'url' => 'acquia-cli',
- ],
- 'Acquia CMS' => [
+ ],
+ 'Acquia CMS' => [
'alias' => ['acquia_cms', 'acms'],
'url' => 'acquia-cms',
- ],
- 'Acquia DAM Classic' => [
+ ],
+ 'Acquia DAM Classic' => [
'alias' => ['dam', 'acquia_dam', 'dam_classic', 'acquiadam', 'damclassic'],
'url' => 'dam',
- ],
- 'Acquia Migrate Accelerate' => [
+ ],
+ 'Acquia Migrate Accelerate' => [
'alias' => ['acquia-migrate-accelerate', 'ama'],
'url' => 'acquia-migrate-accelerate',
- ],
- 'BLT' => [
+ ],
+ 'BLT' => [
'alias' => ['blt'],
'url' => 'blt',
- ],
- 'Campaign Factory' => [
+ ],
+ 'Campaign Factory' => [
'alias' => ['campaign-factory', 'campaign_factory', 'campaignfactory'],
'url' => 'campaign-factory',
- ],
- 'Campaign Studio' => [
+ ],
+ 'Campaign Studio' => [
'alias' => ['campaign-studio', 'campaignstudio'],
'url' => 'campaign-studio',
- ],
- 'Cloud IDE' => [
+ ],
+ 'Cloud IDE' => [
'alias' => ['ide', 'cloud_ide', 'cloud-ide'],
'url' => 'ide',
- ],
- 'Cloud Platform' => [
+ ],
+ 'Cloud Platform' => [
'alias' => ['cloud-platform', 'acquiacloud', 'acquia_cloud', 'acquia-cloud', 'cloud'],
'url' => 'cloud-platform',
- ],
- 'Code Studio' => [
+ ],
+ 'Code Studio' => [
'alias' => ['code_studio', 'codestudio', 'cs'],
'url' => 'code-studio',
- ],
- 'Content Hub' => [
+ ],
+ 'Content Hub' => [
'alias' => ['contenthub', 'ch'],
'url' => 'contenthub',
- ],
- 'Customer Data Platform' => [
+ ],
+ 'Customer Data Platform' => [
'alias' => ['customer-data-platform', 'cdp'],
'url' => 'customer-data-platform',
- ],
- 'Edge' => [
+ ],
+ 'Edge' => [
'alias' => ['edge', 'cloudedge'],
'url' => 'edge',
- ],
- 'Personalization' => [
+ ],
+ 'Personalization' => [
'alias' => ['personalization'],
'url' => 'personalization',
- ],
- 'Search' => [
+ ],
+ 'Search' => [
'alias' => ['search', 'acquia-search'],
'url' => 'acquia-search',
- ],
- 'Shield' => [
+ ],
+ 'Shield' => [
'alias' => ['shield'],
'url' => 'shield',
- ],
- 'Site Factory' => [
+ ],
+ 'Site Factory' => [
'alias' => ['site-factory', 'acsf'],
'url' => 'site-factory',
- ],
- 'Site Studio' => [
+ ],
+ 'Site Studio' => [
'alias' => ['site-studio', 'cohesion'],
'url' => 'site-studio',
- ],
- ];
+ ],
+ ];
- // If user has provided any acquia product in command.
- if ($acquiaProductName = $input->getArgument('product')) {
- $productUrl = NULL;
- foreach ($acquiaProducts as $acquiaProduct) {
- // If product provided by the user exists in the alias.
- if (in_array(strtolower($acquiaProductName), $acquiaProduct['alias'], TRUE)) {
- $productUrl = $acquiaProduct['url'];
- break;
+ // If user has provided any acquia product in command.
+ if ($acquiaProductName = $input->getArgument('product')) {
+ $productUrl = null;
+ foreach ($acquiaProducts as $acquiaProduct) {
+ // If product provided by the user exists in the alias.
+ if (in_array(strtolower($acquiaProductName), $acquiaProduct['alias'], true)) {
+ $productUrl = $acquiaProduct['url'];
+ break;
+ }
+ }
+
+ if ($productUrl) {
+ $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $productUrl . '/');
+ return Command::SUCCESS;
+ }
}
- }
- if ($productUrl) {
- $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $productUrl . '/');
+ $labels = array_keys($acquiaProducts);
+ $question = new ChoiceQuestion('Select the Acquia Product', $labels, $labels[0]);
+ $choiceId = $this->io->askQuestion($question);
+ $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $acquiaProducts[$choiceId]['url'] . '/');
+
return Command::SUCCESS;
- }
}
-
- $labels = array_keys($acquiaProducts);
- $question = new ChoiceQuestion('Select the Acquia Product', $labels, $labels[0]);
- $choiceId = $this->io->askQuestion($question);
- $this->localMachineHelper->startBrowser('https://docs.acquia.com/' . $acquiaProducts[$choiceId]['url'] . '/');
-
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Email/ConfigurePlatformEmailCommand.php b/src/Command/Email/ConfigurePlatformEmailCommand.php
index 3075dd46a..7f7a7e44e 100644
--- a/src/Command/Email/ConfigurePlatformEmailCommand.php
+++ b/src/Command/Email/ConfigurePlatformEmailCommand.php
@@ -1,6 +1,6 @@
addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID to register the domain with.')
- ->setHelp('This command configures Platform Email for a domain in a subscription. It registers the domain with the subscription, associates the domain with an application or set of applications, and enables Platform Email for selected environments of these applications.');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->io->writeln('Welcome to Platform Email setup! This script will walk you through the whole process setting up Platform Email, all through the command line and using the Cloud API!');
- $this->io->writeln('Before getting started, make sure you have the following: ');
-
- $checklist = new Checklist($output);
- $checklist->addItem('the domain name you are registering');
- $checklist->completePreviousItem();
- $checklist->addItem('the subscription where the domain will be registered');
- $checklist->completePreviousItem();
- $checklist->addItem('the application or applications where the domain will be associated');
- $checklist->completePreviousItem();
- $checklist->addItem('the environment or environments for the above applications where Platform Email will be enabled');
- $checklist->completePreviousItem();
- $baseDomain = $this->determineDomain();
- $client = $this->cloudApiClientService->getClient();
- $subscription = $this->determineCloudSubscription();
- $client->request('post', "/subscriptions/$subscription->uuid/domains", [
- 'form_params' => [
- 'domain' => $baseDomain,
- ],
- ]);
-
- $domainUuid = $this->fetchDomainUuid($client, $subscription, $baseDomain);
-
- $this->io->success([
- "Great! You've registered the domain $baseDomain to subscription $subscription->name.",
- "We will create a file with the DNS records for your newly registered domain",
- "Provide these records to your DNS provider",
- "After you've done this, continue to domain verification.",
- ]);
- $fileFormat = $this->io->choice('Would you like your DNS records in BIND Zone File, JSON, or YAML format?', ['BIND Zone File', 'YAML', 'JSON'], 'BIND Zone File');
- $this->createDnsText($client, $subscription, $baseDomain, $domainUuid, $fileFormat);
- $continue = $this->io->confirm('Have you finished providing the DNS records to your DNS provider?');
- if (!$continue) {
- $this->io->info("Make sure to give these records to your DNS provider, then rerun this script with the domain that you just registered.");
- return 1;
+final class ConfigurePlatformEmailCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID to register the domain with.')
+ ->setHelp('This command configures Platform Email for a domain in a subscription. It registers the domain with the subscription, associates the domain with an application or set of applications, and enables Platform Email for selected environments of these applications.');
}
- // Allow for as many reverification tries as needed.
- while (!$this->checkIfDomainVerified($subscription, $domainUuid)) {
- $retryVerification = $this->io->confirm('Would you like to re-check domain verification?');
- if (!$retryVerification) {
- $this->io->writeln('Check your DNS records with your DNS provider and try again by rerunning this script with the domain that you just registered.');
- return 1;
- }
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io->writeln('Welcome to Platform Email setup! This script will walk you through the whole process setting up Platform Email, all through the command line and using the Cloud API!');
+ $this->io->writeln('Before getting started, make sure you have the following: ');
+
+ $checklist = new Checklist($output);
+ $checklist->addItem('the domain name you are registering');
+ $checklist->completePreviousItem();
+ $checklist->addItem('the subscription where the domain will be registered');
+ $checklist->completePreviousItem();
+ $checklist->addItem('the application or applications where the domain will be associated');
+ $checklist->completePreviousItem();
+ $checklist->addItem('the environment or environments for the above applications where Platform Email will be enabled');
+ $checklist->completePreviousItem();
+ $baseDomain = $this->determineDomain();
+ $client = $this->cloudApiClientService->getClient();
+ $subscription = $this->determineCloudSubscription();
+ $client->request('post', "/subscriptions/$subscription->uuid/domains", [
+ 'form_params' => [
+ 'domain' => $baseDomain,
+ ],
+ ]);
+
+ $domainUuid = $this->fetchDomainUuid($client, $subscription, $baseDomain);
+
+ $this->io->success([
+ "Great! You've registered the domain $baseDomain to subscription $subscription->name.",
+ "We will create a file with the DNS records for your newly registered domain",
+ "Provide these records to your DNS provider",
+ "After you've done this, continue to domain verification.",
+ ]);
+ $fileFormat = $this->io->choice('Would you like your DNS records in BIND Zone File, JSON, or YAML format?', ['BIND Zone File', 'YAML', 'JSON'], 'BIND Zone File');
+ $this->createDnsText($client, $subscription, $baseDomain, $domainUuid, $fileFormat);
+ $continue = $this->io->confirm('Have you finished providing the DNS records to your DNS provider?');
+ if (!$continue) {
+ $this->io->info("Make sure to give these records to your DNS provider, then rerun this script with the domain that you just registered.");
+ return 1;
+ }
- $this->io->success("The next step is associating your verified domain with an application (or applications) in the subscription where your domain has been registered.");
+ // Allow for as many reverification tries as needed.
+ while (!$this->checkIfDomainVerified($subscription, $domainUuid)) {
+ $retryVerification = $this->io->confirm('Would you like to re-check domain verification?');
+ if (!$retryVerification) {
+ $this->io->writeln('Check your DNS records with your DNS provider and try again by rerunning this script with the domain that you just registered.');
+ return 1;
+ }
+ }
- if (!$this->addDomainToSubscriptionApplications($client, $subscription, $baseDomain, $domainUuid)) {
- $this->io->error('Something went wrong with associating your application(s) or enabling your environment(s). Try again.');
- return 1;
- }
+ $this->io->success("The next step is associating your verified domain with an application (or applications) in the subscription where your domain has been registered.");
- $this->io->success("You're all set to start using Platform Email!");
-
- return Command::SUCCESS;
- }
-
- /**
- * Generates Zone File for DNS records of the registered domain.
- */
- private function generateZoneFile(string $baseDomain, array $records): void {
-
- $zone = new Zone($baseDomain . '.');
-
- foreach ($records as $record) {
- unset($record->health);
- $recordToAdd = $zone->getNode($record->name . '.');
-
- switch ($record->type) {
- case 'MX':
- $mxPriorityValueArr = explode(' ', $record->value);
- $recordToAdd->getRecordAppender()->appendMxRecord((int) $mxPriorityValueArr[0], $mxPriorityValueArr[1] . '.', 3600);
- break;
- case 'TXT':
- $recordToAdd->getRecordAppender()->appendTxtRecord($record->value, 3600);
- break;
- case 'CNAME':
- $recordToAdd->getRecordAppender()->appendCNameRecord($record->value . '.', 3600);
- break;
- }
- }
+ if (!$this->addDomainToSubscriptionApplications($client, $subscription, $baseDomain, $domainUuid)) {
+ $this->io->error('Something went wrong with associating your application(s) or enabling your environment(s). Try again.');
+ return 1;
+ }
- $this->localMachineHelper->getFilesystem()
- ->dumpFile('dns-records.zone', (string) $zone);
+ $this->io->success("You're all set to start using Platform Email!");
- }
+ return Command::SUCCESS;
+ }
- /**
- * Determines the applications for domain association and environment
- * enablement of Platform Email.
- *
- * @return array
- */
- private function determineApplications(Client $client, SubscriptionResponse $subscription): array {
- $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription);
+ /**
+ * Generates Zone File for DNS records of the registered domain.
+ */
+ private function generateZoneFile(string $baseDomain, array $records): void
+ {
+
+ $zone = new Zone($baseDomain . '.');
+
+ foreach ($records as $record) {
+ unset($record->health);
+ $recordToAdd = $zone->getNode($record->name . '.');
+
+ switch ($record->type) {
+ case 'MX':
+ $mxPriorityValueArr = explode(' ', $record->value);
+ $recordToAdd->getRecordAppender()->appendMxRecord((int) $mxPriorityValueArr[0], $mxPriorityValueArr[1] . '.', 3600);
+ break;
+ case 'TXT':
+ $recordToAdd->getRecordAppender()->appendTxtRecord($record->value, 3600);
+ break;
+ case 'CNAME':
+ $recordToAdd->getRecordAppender()->appendCNameRecord($record->value . '.', 3600);
+ break;
+ }
+ }
- if (count($subscriptionApplications) === 1) {
- $applications = $subscriptionApplications;
- $this->io->info("You have one application, {$applications[0]->name}, in this subscription.");
+ $this->localMachineHelper->getFilesystem()
+ ->dumpFile('dns-records.zone', (string) $zone);
}
- else {
- $applications = $this->promptChooseFromObjectsOrArrays($subscriptionApplications, 'uuid', 'name', "What are the applications you'd like to associate this domain with? You may enter multiple separated by a comma.", TRUE);
+
+ /**
+ * Determines the applications for domain association and environment
+ * enablement of Platform Email.
+ *
+ * @return array
+ */
+ private function determineApplications(Client $client, SubscriptionResponse $subscription): array
+ {
+ $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription);
+
+ if (count($subscriptionApplications) === 1) {
+ $applications = $subscriptionApplications;
+ $this->io->info("You have one application, {$applications[0]->name}, in this subscription.");
+ } else {
+ $applications = $this->promptChooseFromObjectsOrArrays($subscriptionApplications, 'uuid', 'name', "What are the applications you'd like to associate this domain with? You may enter multiple separated by a comma.", true);
+ }
+ return $applications;
}
- return $applications;
- }
-
- /**
- * Checks any error from Cloud API when associating a domain with an
- * application. Shows a warning and allows user to continue if the domain has
- * been associated already. For any other error from the API, the setup will
- * exit.
- */
- private function domainAlreadyAssociated(object $application, ApiErrorException $exception): ?bool {
- if (!str_contains($exception->getMessage(), 'is already associated with this application')) {
- $this->io->error($exception->getMessage());
- return FALSE;
+
+ /**
+ * Checks any error from Cloud API when associating a domain with an
+ * application. Shows a warning and allows user to continue if the domain has
+ * been associated already. For any other error from the API, the setup will
+ * exit.
+ */
+ private function domainAlreadyAssociated(object $application, ApiErrorException $exception): ?bool
+ {
+ if (!str_contains($exception->getMessage(), 'is already associated with this application')) {
+ $this->io->error($exception->getMessage());
+ return false;
+ }
+
+ $this->io->warning($application->name . ' - ' . $exception->getMessage());
+ return true;
}
- $this->io->warning($application->name . ' - ' . $exception->getMessage());
- return TRUE;
- }
-
- /**
- * Checks any error from Cloud API when enabling Platform Email for an
- * environment. Shows a warning and allows user to continue if Platform Email
- * has already been enabled for the environment. For any other error from the
- * API, the setup will exit.
- */
- private function environmentAlreadyEnabled(object $environment, ApiErrorException $exception): ?bool {
- if (!str_contains($exception->getMessage(), 'is already enabled on this environment')) {
- $this->io->error($exception->getMessage());
- return FALSE;
+ /**
+ * Checks any error from Cloud API when enabling Platform Email for an
+ * environment. Shows a warning and allows user to continue if Platform Email
+ * has already been enabled for the environment. For any other error from the
+ * API, the setup will exit.
+ */
+ private function environmentAlreadyEnabled(object $environment, ApiErrorException $exception): ?bool
+ {
+ if (!str_contains($exception->getMessage(), 'is already enabled on this environment')) {
+ $this->io->error($exception->getMessage());
+ return false;
+ }
+
+ $this->io->warning($environment->label . ' - ' . $exception->getMessage());
+ return true;
}
- $this->io->warning($environment->label . ' - ' . $exception->getMessage());
- return TRUE;
- }
-
- /**
- * Associates a domain with an application or applications,
- * then enables Platform Email for an environment or environments
- * of the above applications.
- */
- private function addDomainToSubscriptionApplications(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid): bool {
- $applications = $this->determineApplications($client, $subscription);
-
- $environmentsResource = new Environments($client);
- foreach ($applications as $application) {
- try {
- $client->request('post', "/applications/$application->uuid/email/domains/$domainUuid/actions/associate");
- $this->io->success("Domain $baseDomain has been associated with Application $application->name");
- }
- catch (ApiErrorException $e) {
- if (!$this->domainAlreadyAssociated($application, $e)) {
- return FALSE;
+ /**
+ * Associates a domain with an application or applications,
+ * then enables Platform Email for an environment or environments
+ * of the above applications.
+ */
+ private function addDomainToSubscriptionApplications(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid): bool
+ {
+ $applications = $this->determineApplications($client, $subscription);
+
+ $environmentsResource = new Environments($client);
+ foreach ($applications as $application) {
+ try {
+ $client->request('post', "/applications/$application->uuid/email/domains/$domainUuid/actions/associate");
+ $this->io->success("Domain $baseDomain has been associated with Application $application->name");
+ } catch (ApiErrorException $e) {
+ if (!$this->domainAlreadyAssociated($application, $e)) {
+ return false;
+ }
+ }
+
+ $applicationEnvironments = $environmentsResource->getAll($application->uuid);
+ $envs = $this->promptChooseFromObjectsOrArrays(
+ $applicationEnvironments,
+ 'uuid',
+ 'label',
+ "What are the environments of $application->name that you'd like to enable email for? You may enter multiple separated by a comma.",
+ true
+ );
+ foreach ($envs as $env) {
+ try {
+ $client->request('post', "/environments/$env->uuid/email/actions/enable");
+ $this->io->success("Platform Email has been enabled for environment $env->label for application $application->name");
+ } catch (ApiErrorException $e) {
+ if (!$this->environmentAlreadyEnabled($env, $e)) {
+ return false;
+ }
+ }
+ }
}
- }
-
- $applicationEnvironments = $environmentsResource->getAll($application->uuid);
- $envs = $this->promptChooseFromObjectsOrArrays(
- $applicationEnvironments,
- 'uuid',
- 'label',
- "What are the environments of $application->name that you'd like to enable email for? You may enter multiple separated by a comma.",
- TRUE
- );
- foreach ($envs as $env) {
- try {
- $client->request('post', "/environments/$env->uuid/email/actions/enable");
- $this->io->success("Platform Email has been enabled for environment $env->label for application $application->name");
+ return true;
+ }
+
+ /**
+ * Validates the URL entered as the base domain name.
+ */
+ public static function validateUrl(string $url): string
+ {
+ $constraintsList = [new NotBlank()];
+ $urlParts = parse_url($url);
+ if (array_key_exists('host', $urlParts)) {
+ $constraintsList[] = new Url();
+ } else {
+ $constraintsList[] = new Hostname();
}
- catch (ApiErrorException $e) {
- if (!$this->environmentAlreadyEnabled($env, $e)) {
- return FALSE;
- }
+ $violations = Validation::createValidator()->validate($url, $constraintsList);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
}
- }
- }
- return TRUE;
- }
-
- /**
- * Validates the URL entered as the base domain name.
- */
- public static function validateUrl(string $url): string {
- $constraintsList = [new NotBlank()];
- $urlParts = parse_url($url);
- if (array_key_exists('host', $urlParts)) {
- $constraintsList[] = new Url();
- }
- else {
- $constraintsList[] = new Hostname();
- }
- $violations = Validation::createValidator()->validate($url, $constraintsList);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
- }
- return $url;
- }
-
- /**
- * Retrieves a domain registration UUID given the domain name.
- */
- private function fetchDomainUuid(Client $client, SubscriptionResponse $subscription, string $baseDomain): mixed {
- $domainsResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains");
- foreach ($domainsResponse as $domain) {
- if ($domain->domain_name === $baseDomain) {
- return $domain->uuid;
- }
+ return $url;
}
- throw new AcquiaCliException("Could not find domain $baseDomain");
- }
-
- /**
- * Creates a file, either in Bind Zone File, JSON or YAML format,
- * of the DNS records needed to complete Platform Email setup.
- */
- private function createDnsText(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid, string $fileFormat): void {
- $domainRegistrationResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid");
- if (!isset($domainRegistrationResponse->dns_records)) {
- throw new AcquiaCliException('Could not retrieve DNS records for this domain. Try again by rerunning this script with the domain that you just registered.');
+
+ /**
+ * Retrieves a domain registration UUID given the domain name.
+ */
+ private function fetchDomainUuid(Client $client, SubscriptionResponse $subscription, string $baseDomain): mixed
+ {
+ $domainsResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains");
+ foreach ($domainsResponse as $domain) {
+ if ($domain->domain_name === $baseDomain) {
+ return $domain->uuid;
+ }
+ }
+ throw new AcquiaCliException("Could not find domain $baseDomain");
}
- $records = [];
- $this->localMachineHelper->getFilesystem()->remove('dns-records.json');
- $this->localMachineHelper->getFilesystem()->remove('dns-records.yaml');
- $this->localMachineHelper->getFilesystem()->remove('dns-records.zone');
- if ($fileFormat === 'JSON') {
- foreach ($domainRegistrationResponse->dns_records as $record) {
- unset($record->health);
- $records[] = $record;
- }
- $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR));
- $this->localMachineHelper->getFilesystem()
+
+ /**
+ * Creates a file, either in Bind Zone File, JSON or YAML format,
+ * of the DNS records needed to complete Platform Email setup.
+ */
+ private function createDnsText(Client $client, SubscriptionResponse $subscription, string $baseDomain, string $domainUuid, string $fileFormat): void
+ {
+ $domainRegistrationResponse = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid");
+ if (!isset($domainRegistrationResponse->dns_records)) {
+ throw new AcquiaCliException('Could not retrieve DNS records for this domain. Try again by rerunning this script with the domain that you just registered.');
+ }
+ $records = [];
+ $this->localMachineHelper->getFilesystem()->remove('dns-records.json');
+ $this->localMachineHelper->getFilesystem()->remove('dns-records.yaml');
+ $this->localMachineHelper->getFilesystem()->remove('dns-records.zone');
+ if ($fileFormat === 'JSON') {
+ foreach ($domainRegistrationResponse->dns_records as $record) {
+ unset($record->health);
+ $records[] = $record;
+ }
+ $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR));
+ $this->localMachineHelper->getFilesystem()
->dumpFile('dns-records.json', json_encode($records, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
- }
- else if ($fileFormat === 'YAML') {
- foreach ($domainRegistrationResponse->dns_records as $record) {
- unset($record->health);
- $records[] = ['type' => $record->type, 'name' => $record->name, 'value' => $record->value];
- }
- $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR));
- $this->localMachineHelper->getFilesystem()
+ } elseif ($fileFormat === 'YAML') {
+ foreach ($domainRegistrationResponse->dns_records as $record) {
+ unset($record->health);
+ $records[] = ['type' => $record->type, 'name' => $record->name, 'value' => $record->value];
+ }
+ $this->logger->debug(json_encode($records, JSON_THROW_ON_ERROR));
+ $this->localMachineHelper->getFilesystem()
->dumpFile('dns-records.yaml', Yaml::dump($records));
- }
- else {
- $this->generateZoneFile($baseDomain, $domainRegistrationResponse->dns_records);
+ } else {
+ $this->generateZoneFile($baseDomain, $domainRegistrationResponse->dns_records);
+ }
}
- }
-
- /**
- * Checks the verification status of the registered domain.
- */
- private function checkIfDomainVerified(
- SubscriptionResponse $subscription,
- string $domainUuid
- ): bool {
- $client = $this->cloudApiClientService->getClient();
- try {
- $response = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid");
- if (isset($response->health) && $response->health->code === "200") {
- $this->io->success("Your domain is ready for use!");
- return TRUE;
- }
-
- // @infection-ignore-all
- if (isset($response->health) && str_starts_with($response->health->code, "4")) {
- $this->io->error($response->health->details);
- if ($this->io->confirm('Would you like to refresh?')) {
- $client->request('post', "/subscriptions/$subscription->uuid/domains/$domainUuid/actions/verify");
- $this->io->info('Refreshing...');
+ /**
+ * Checks the verification status of the registered domain.
+ */
+ private function checkIfDomainVerified(
+ SubscriptionResponse $subscription,
+ string $domainUuid
+ ): bool {
+ $client = $this->cloudApiClientService->getClient();
+ try {
+ $response = $client->request('get', "/subscriptions/$subscription->uuid/domains/$domainUuid");
+ if (isset($response->health) && $response->health->code === "200") {
+ $this->io->success("Your domain is ready for use!");
+ return true;
+ }
+
+ // @infection-ignore-all
+ if (isset($response->health) && str_starts_with($response->health->code, "4")) {
+ $this->io->error($response->health->details);
+ if ($this->io->confirm('Would you like to refresh?')) {
+ $client->request('post', "/subscriptions/$subscription->uuid/domains/$domainUuid/actions/verify");
+ $this->io->info('Refreshing...');
+ }
+ }
+
+ $this->io->info("Verification pending...");
+ $this->logger->debug(json_encode($response, JSON_THROW_ON_ERROR));
+ return false;
+ } catch (AcquiaCliException $exception) {
+ $this->logger->debug($exception->getMessage());
+ return false;
}
- }
-
- $this->io->info("Verification pending...");
- $this->logger->debug(json_encode($response, JSON_THROW_ON_ERROR));
- return FALSE;
- }
- catch (AcquiaCliException $exception) {
- $this->logger->debug($exception->getMessage());
- return FALSE;
}
- }
-
- /**
- * Finds, validates, and trims the URL to be used as the base domain
- * for setting up Platform Email.
- */
- private function determineDomain(): string {
- $domain = $this->io->ask("What's the domain name you'd like to register?", '', Closure::fromCallable([
- $this,
- 'validateUrl',
- ]));
-
- $domainParts = parse_url($domain);
- if (array_key_exists('host', $domainParts)) {
- $return = $domainParts['host'];
- }
- else {
- $return = $domain;
- }
- return str_replace('www.', '', $return);
- }
+ /**
+ * Finds, validates, and trims the URL to be used as the base domain
+ * for setting up Platform Email.
+ */
+ private function determineDomain(): string
+ {
+ $domain = $this->io->ask("What's the domain name you'd like to register?", '', Closure::fromCallable([
+ $this,
+ 'validateUrl',
+ ]));
+
+ $domainParts = parse_url($domain);
+ if (array_key_exists('host', $domainParts)) {
+ $return = $domainParts['host'];
+ } else {
+ $return = $domain;
+ }
+ return str_replace('www.', '', $return);
+ }
}
diff --git a/src/Command/Email/EmailInfoForSubscriptionCommand.php b/src/Command/Email/EmailInfoForSubscriptionCommand.php
index 11196015f..f4b6fcd3e 100644
--- a/src/Command/Email/EmailInfoForSubscriptionCommand.php
+++ b/src/Command/Email/EmailInfoForSubscriptionCommand.php
@@ -1,6 +1,6 @@
addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID whose Platform Email configuration is to be checked.')
- ->setHelp('This command lists information related to Platform Email for a subscription, including which domains have been validated, which have not, and which applications have Platform Email domains associated.');
- }
+final class EmailInfoForSubscriptionCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('subscriptionUuid', InputArgument::OPTIONAL, 'The subscription UUID whose Platform Email configuration is to be checked.')
+ ->setHelp('This command lists information related to Platform Email for a subscription, including which domains have been validated, which have not, and which applications have Platform Email domains associated.');
+ }
- protected function execute(InputInterface $input, OutputInterface $output): int {
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
- $client = $this->cloudApiClientService->getClient();
- $subscription = $this->determineCloudSubscription();
+ $client = $this->cloudApiClientService->getClient();
+ $subscription = $this->determineCloudSubscription();
- $response = $client->request('get', "/subscriptions/$subscription->uuid/domains");
+ $response = $client->request('get', "/subscriptions/$subscription->uuid/domains");
- if (count($response)) {
+ if (count($response)) {
+ $this->localMachineHelper->getFilesystem()->remove("./subscription-$subscription->uuid-domains");
+ $this->localMachineHelper->getFilesystem()->mkdir("./subscription-$subscription->uuid-domains");
- $this->localMachineHelper->getFilesystem()->remove("./subscription-$subscription->uuid-domains");
- $this->localMachineHelper->getFilesystem()->mkdir("./subscription-$subscription->uuid-domains");
+ $this->writeDomainsToTables($output, $subscription, $response);
- $this->writeDomainsToTables($output, $subscription, $response);
+ $subscriptionApplications = $this->validateSubscriptionApplicationCount($client, $subscription);
- $subscriptionApplications = $this->validateSubscriptionApplicationCount($client, $subscription);
+ if (!isset($subscriptionApplications)) {
+ return 1;
+ }
- if (!isset($subscriptionApplications)) {
- return 1;
- }
+ $this->renderApplicationAssociations($output, $client, $subscription, $subscriptionApplications);
- $this->renderApplicationAssociations($output, $client, $subscription, $subscriptionApplications);
+ $this->output->writeln("CSV files with these tables have been exported to /subscription-$subscription->uuid-domains>. A detailed breakdown of each domain's DNS records has been exported there as well.");
+ } else {
+ $this->io->info("No email domains registered in $subscription->name.");
+ }
- $this->output->writeln("CSV files with these tables have been exported to /subscription-$subscription->uuid-domains>. A detailed breakdown of each domain's DNS records has been exported there as well.");
- }
- else {
- $this->io->info("No email domains registered in $subscription->name.");
+ return Command::SUCCESS;
}
- return Command::SUCCESS;
- }
-
- /**
- * Renders tables showing email domain verification statuses,
- * as well as exports these statuses to respective CSV files.
- */
- private function writeDomainsToTables(OutputInterface $output, SubscriptionResponse $subscription, array $domainList): void {
-
- // Initialize tables to be displayed in console.
- $allDomainsTable = $this->createTotalDomainTable($output, "Subscription $subscription->name - All Domains");
- $verifiedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Verified Domains");
- $pendingDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Pending Domains");
- $failedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Failed Domains");
-
- // Initialize csv writers for each file.
- $writerAllDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-summary.csv", 'w+');
- $writerVerifiedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/verified-domains-summary.csv", 'w+');
- $writerPendingDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/pending-domains-summary.csv", 'w+');
- $writerFailedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/failed-domains-summary.csv", 'w+');
- $writerAllDomainsDnsHealth = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-dns-health.csv", 'w+');
-
- $allDomainsSummaryHeader = ['Domain Name', 'Domain UUID', 'Verification Status'];
- $writerAllDomains->insertOne($allDomainsSummaryHeader);
-
- $verifiedDomainsHeader = ['Domain Name', 'Summary'];
- $writerVerifiedDomains->insertOne($verifiedDomainsHeader);
-
- $pendingDomainsHeader = $verifiedDomainsHeader;
- $writerPendingDomains->insertOne($pendingDomainsHeader);
-
- $failedDomainsHeader = $verifiedDomainsHeader;
- $writerFailedDomains->insertOne($failedDomainsHeader);
-
- $allDomainsDnsHealthCsvHeader = ['Domain Name', 'Domain UUID', 'Domain Health', 'DNS Record Name', 'DNS Record Type', 'DNS Record Value', 'DNS Record Health Details'];
- $writerAllDomainsDnsHealth->insertOne($allDomainsDnsHealthCsvHeader);
-
- foreach ($domainList as $domain) {
- $domainNameAndSummary = [$domain->domain_name, $domain->health->summary];
-
- if ($domain->health->code === '200') {
- $verifiedDomainsTable->addRow($domainNameAndSummary);
- $writerVerifiedDomains->insertOne($domainNameAndSummary);
- }
- else if ($domain->health->code === '202') {
- $pendingDomainsTable->addRow($domainNameAndSummary);
- $writerPendingDomains->insertOne($domainNameAndSummary);
- }
- else {
- $failedDomainsTable->addRow($domainNameAndSummary);
- $writerFailedDomains->insertOne($domainNameAndSummary);
- }
-
- $allDomainsTable->addRow([
- $domain->domain_name,
- $domain->uuid,
- $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code,
- ]);
-
- $writerAllDomains->insertOne([
- $domain->domain_name,
- $domain->uuid,
- $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code,
- ]);
-
- foreach ($domain->dns_records as $index => $record) {
- if ($index === 0) {
- $writerAllDomainsDnsHealth->insertOne([
+ /**
+ * Renders tables showing email domain verification statuses,
+ * as well as exports these statuses to respective CSV files.
+ */
+ private function writeDomainsToTables(OutputInterface $output, SubscriptionResponse $subscription, array $domainList): void
+ {
+
+ // Initialize tables to be displayed in console.
+ $allDomainsTable = $this->createTotalDomainTable($output, "Subscription $subscription->name - All Domains");
+ $verifiedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Verified Domains");
+ $pendingDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Pending Domains");
+ $failedDomainsTable = $this->createDomainStatusTable($output, "Subscription $subscription->name - Failed Domains");
+
+ // Initialize csv writers for each file.
+ $writerAllDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-summary.csv", 'w+');
+ $writerVerifiedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/verified-domains-summary.csv", 'w+');
+ $writerPendingDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/pending-domains-summary.csv", 'w+');
+ $writerFailedDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/failed-domains-summary.csv", 'w+');
+ $writerAllDomainsDnsHealth = Writer::createFromPath("./subscription-$subscription->uuid-domains/all-domains-dns-health.csv", 'w+');
+
+ $allDomainsSummaryHeader = ['Domain Name', 'Domain UUID', 'Verification Status'];
+ $writerAllDomains->insertOne($allDomainsSummaryHeader);
+
+ $verifiedDomainsHeader = ['Domain Name', 'Summary'];
+ $writerVerifiedDomains->insertOne($verifiedDomainsHeader);
+
+ $pendingDomainsHeader = $verifiedDomainsHeader;
+ $writerPendingDomains->insertOne($pendingDomainsHeader);
+
+ $failedDomainsHeader = $verifiedDomainsHeader;
+ $writerFailedDomains->insertOne($failedDomainsHeader);
+
+ $allDomainsDnsHealthCsvHeader = ['Domain Name', 'Domain UUID', 'Domain Health', 'DNS Record Name', 'DNS Record Type', 'DNS Record Value', 'DNS Record Health Details'];
+ $writerAllDomainsDnsHealth->insertOne($allDomainsDnsHealthCsvHeader);
+
+ foreach ($domainList as $domain) {
+ $domainNameAndSummary = [$domain->domain_name, $domain->health->summary];
+
+ if ($domain->health->code === '200') {
+ $verifiedDomainsTable->addRow($domainNameAndSummary);
+ $writerVerifiedDomains->insertOne($domainNameAndSummary);
+ } elseif ($domain->health->code === '202') {
+ $pendingDomainsTable->addRow($domainNameAndSummary);
+ $writerPendingDomains->insertOne($domainNameAndSummary);
+ } else {
+ $failedDomainsTable->addRow($domainNameAndSummary);
+ $writerFailedDomains->insertOne($domainNameAndSummary);
+ }
+
+ $allDomainsTable->addRow([
+ $domain->domain_name,
+ $domain->uuid,
+ $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code,
+ ]);
+
+ $writerAllDomains->insertOne([
$domain->domain_name,
$domain->uuid,
$this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code,
- $record->name,
- $record->type,
- $record->value,
- $record->health->details,
- ]);
+ ]);
+
+ foreach ($domain->dns_records as $index => $record) {
+ if ($index === 0) {
+ $writerAllDomainsDnsHealth->insertOne([
+ $domain->domain_name,
+ $domain->uuid,
+ $this->showHumanReadableStatus($domain->health->code) . ' - ' . $domain->health->code,
+ $record->name,
+ $record->type,
+ $record->value,
+ $record->health->details,
+ ]);
+ } else {
+ $writerAllDomainsDnsHealth->insertOne([
+ '',
+ '',
+ '',
+ $record->name,
+ $record->type,
+ $record->value,
+ $record->health->details,
+ ]);
+ }
+ }
}
- else {
- $writerAllDomainsDnsHealth->insertOne([
- '',
- '',
- '',
- $record->name,
- $record->type,
- $record->value,
- $record->health->details,
- ]);
+
+ $this->renderDomainInfoTables([$allDomainsTable, $verifiedDomainsTable, $pendingDomainsTable, $failedDomainsTable]);
+ }
+
+ /**
+ * Nicely renders a given array of tables.
+ */
+ private function renderDomainInfoTables(array $tables): void
+ {
+ foreach ($tables as $table) {
+ $table->render();
+ $this->io->newLine();
}
- }
}
- $this->renderDomainInfoTables([$allDomainsTable, $verifiedDomainsTable, $pendingDomainsTable, $failedDomainsTable]);
+ /**
+ * Verifies the number of applications present in a subscription.
+ *
+ * @return array|null
+ */
+ private function validateSubscriptionApplicationCount(Client $client, SubscriptionResponse $subscription): ?array
+ {
+ $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription);
+ if (count($subscriptionApplications) > 100) {
+ $this->io->warning('You have over 100 applications in this subscription. Retrieving the email domains for each could take a while!');
+ $continue = $this->io->confirm('Do you wish to continue?');
+ if (!$continue) {
+ return null;
+ }
+ }
- }
+ return $subscriptionApplications;
+ }
+
+ /**
+ * Renders a table of applications in a subscription and the email domains
+ * associated or dissociated with each application.
+ *
+ * @param $subscription
+ * @param $subscriptionApplications
+ */
+ private function renderApplicationAssociations(OutputInterface $output, Client $client, \AcquiaCloudApi\Response\SubscriptionResponse $subscription, array $subscriptionApplications): void
+ {
+ $appsDomainsTable = $this->createApplicationDomainsTable($output);
+ $writerAppsDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/apps-domain-associations.csv", 'w+');
+
+ $appsDomainsHeader = ['Application', 'Domain Name', 'Associated?'];
+ $writerAppsDomains->insertOne($appsDomainsHeader);
+
+ foreach ($subscriptionApplications as $index => $app) {
+ $appDomains = $client->request('get', "/applications/$app->uuid/email/domains");
+
+ if ($index !== 0) {
+ $appsDomainsTable->addRow([new TableSeparator(['colspan' => 2])]);
+ }
+ $appsDomainsTable->addRow([new TableCell("Application: $app->name", ['colspan' => 2])]);
+ if (count($appDomains)) {
+ foreach ($appDomains as $domain) {
+ $appsDomainsTable->addRow([
+ $domain->domain_name,
+ var_export($domain->flags->associated, true),
+ ]);
+ $writerAppsDomains->insertOne([$app->name, $domain->domain_name, var_export($domain->flags->associated, true)]);
+ }
+ } else {
+ $appsDomainsTable->addRow([new TableCell("No domains eligible for association.", [
+ 'colspan' => 2,
+ 'style' => new TableCellStyle([
+ 'fg' => 'yellow',
+ ]),
+ ]),
+ ]);
+ $writerAppsDomains->insertOne([$app->name, 'No domains eligible for association', '']);
+ }
+ }
+ $appsDomainsTable->render();
+ }
- /**
- * Nicely renders a given array of tables.
- */
- private function renderDomainInfoTables(array $tables): void {
- foreach ($tables as $table) {
- $table->render();
- $this->io->newLine();
+ /**
+ * Creates a table of all domains registered in a subscription.
+ */
+ private function createTotalDomainTable(OutputInterface $output, string $title): Table
+ {
+ $headers = ['Domain Name', 'Domain UUID', 'Verification Status'];
+ $widths = [.2, .2, .1];
+ return $this->createTable($output, $title, $headers, $widths);
}
- }
-
- /**
- * Verifies the number of applications present in a subscription.
- *
- * @return array|null
- */
- private function validateSubscriptionApplicationCount(Client $client, SubscriptionResponse $subscription): ?array {
- $subscriptionApplications = $this->getSubscriptionApplications($client, $subscription);
- if (count($subscriptionApplications) > 100) {
- $this->io->warning('You have over 100 applications in this subscription. Retrieving the email domains for each could take a while!');
- $continue = $this->io->confirm('Do you wish to continue?');
- if (!$continue) {
- return NULL;
- }
+
+ /**
+ * Creates a table of domains of one verification status in a subscription.
+ */
+ private function createDomainStatusTable(OutputInterface $output, string $title): Table
+ {
+ $headers = ['Domain Name', 'Summary'];
+ $widths = [.2, .2];
+ return $this->createTable($output, $title, $headers, $widths);
}
- return $subscriptionApplications;
- }
-
- /**
- * Renders a table of applications in a subscription and the email domains
- * associated or dissociated with each application.
- *
- * @param $subscription
- * @param $subscriptionApplications
- */
- private function renderApplicationAssociations(OutputInterface $output, Client $client, \AcquiaCloudApi\Response\SubscriptionResponse $subscription, array $subscriptionApplications): void {
- $appsDomainsTable = $this->createApplicationDomainsTable($output);
- $writerAppsDomains = Writer::createFromPath("./subscription-$subscription->uuid-domains/apps-domain-associations.csv", 'w+');
-
- $appsDomainsHeader = ['Application', 'Domain Name', 'Associated?'];
- $writerAppsDomains->insertOne($appsDomainsHeader);
-
- foreach ($subscriptionApplications as $index => $app) {
- $appDomains = $client->request('get', "/applications/$app->uuid/email/domains");
-
- if ($index !== 0) {
- $appsDomainsTable->addRow([new TableSeparator(['colspan' => 2])]);
- }
- $appsDomainsTable->addRow([new TableCell("Application: $app->name", ['colspan' => 2])]);
- if (count($appDomains)) {
- foreach ($appDomains as $domain) {
- $appsDomainsTable->addRow([
- $domain->domain_name,
- var_export($domain->flags->associated, TRUE),
- ]);
- $writerAppsDomains->insertOne([$app->name, $domain->domain_name, var_export($domain->flags->associated, TRUE)]);
- }
- }
- else {
- $appsDomainsTable->addRow([new TableCell("No domains eligible for association.", [
- 'colspan' => 2,
- 'style' => new TableCellStyle([
- 'fg' => 'yellow',
- ]),
- ]),
- ]);
- $writerAppsDomains->insertOne([$app->name, 'No domains eligible for association', '']);
- }
+ /**
+ * Creates a table of applications in a subscription and the associated
+ * or dissociated domains in each application.
+ */
+ private function createApplicationDomainsTable(OutputInterface $output): Table
+ {
+ $headers = ['Domain Name', 'Associated?'];
+ $widths = [.2, .1];
+ return $this->createTable($output, 'Domain Association Status', $headers, $widths);
}
- $appsDomainsTable->render();
- }
-
- /**
- * Creates a table of all domains registered in a subscription.
- */
- private function createTotalDomainTable(OutputInterface $output, string $title): Table {
- $headers = ['Domain Name', 'Domain UUID', 'Verification Status'];
- $widths = [.2, .2, .1];
- return $this->createTable($output, $title, $headers, $widths);
- }
-
- /**
- * Creates a table of domains of one verification status in a subscription.
- */
- private function createDomainStatusTable(OutputInterface $output, string $title): Table {
- $headers = ['Domain Name', 'Summary'];
- $widths = [.2, .2];
- return $this->createTable($output, $title, $headers, $widths);
- }
-
- /**
- * Creates a table of applications in a subscription and the associated
- * or dissociated domains in each application.
- */
- private function createApplicationDomainsTable(OutputInterface $output): Table {
- $headers = ['Domain Name', 'Associated?'];
- $widths = [.2, .1];
- return $this->createTable($output, 'Domain Association Status', $headers, $widths);
- }
-
- /**
- * Returns a human-readable string of whether a status code represents
- * a failed, pending, or successful domain verification.
- */
- private function showHumanReadableStatus(string $code): string {
- return match ($code) {
- '200' => "Succeeded",
- '202' => "Pending",
- default => "Failed",
- };
- }
+ /**
+ * Returns a human-readable string of whether a status code represents
+ * a failed, pending, or successful domain verification.
+ */
+ private function showHumanReadableStatus(string $code): string
+ {
+ return match ($code) {
+ '200' => "Succeeded",
+ '202' => "Pending",
+ default => "Failed",
+ };
+ }
}
diff --git a/src/Command/Env/EnvCertCreateCommand.php b/src/Command/Env/EnvCertCreateCommand.php
index a68051f82..2e94b0a51 100644
--- a/src/Command/Env/EnvCertCreateCommand.php
+++ b/src/Command/Env/EnvCertCreateCommand.php
@@ -1,6 +1,6 @@
addArgument('certificate', InputArgument::REQUIRED, 'Filename of the SSL certificate being installed')
- ->addArgument('private-key', InputArgument::REQUIRED, 'Filename of the SSL private key')
- ->addOption('legacy', '', InputOption::VALUE_OPTIONAL, 'True for legacy certificates', FALSE)
- ->addOption('ca-certificates', '', InputOption::VALUE_OPTIONAL, 'Filename of the CA intermediary certificates')
- ->addOption('csr-id', '', InputOption::VALUE_OPTIONAL, 'The CSR (certificate signing request) to associate with this certificate')
- ->addOption('label', '', InputOption::VALUE_OPTIONAL, 'The label for this certificate. Required for standard certificates. Optional for legacy certificates', 'My certificate')
- ->acceptEnvironmentId();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $environment = $this->determineEnvironment($input, $output, TRUE, TRUE);
- $certificate = $input->getArgument('certificate');
- $privateKey = $input->getArgument('private-key');
- $label = $this->determineOption('label');
- $caCertificates = $this->determineOption('ca-certificates');
- $csrId = (int) $this->determineOption('csr-id');
- $legacy = $this->determineOption('legacy', FALSE, NULL, NULL, 'false');
- $legacy = filter_var($legacy, FILTER_VALIDATE_BOOLEAN);
-
- $sslCertificates = new SslCertificates($acquiaCloudClient);
- $response = $sslCertificates->create(
- $environment->uuid,
- $label,
- $this->localMachineHelper->readFile($certificate),
- $this->localMachineHelper->readFile($privateKey),
- $caCertificates ? $this->localMachineHelper->readFile($caCertificates) : NULL,
- $csrId,
- $legacy
- );
- $notificationUuid = CommandBase::getNotificationUuidFromResponse($response);
- $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate');
- return Command::SUCCESS;
- }
-
+final class EnvCertCreateCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('certificate', InputArgument::REQUIRED, 'Filename of the SSL certificate being installed')
+ ->addArgument('private-key', InputArgument::REQUIRED, 'Filename of the SSL private key')
+ ->addOption('legacy', '', InputOption::VALUE_OPTIONAL, 'True for legacy certificates', false)
+ ->addOption('ca-certificates', '', InputOption::VALUE_OPTIONAL, 'Filename of the CA intermediary certificates')
+ ->addOption('csr-id', '', InputOption::VALUE_OPTIONAL, 'The CSR (certificate signing request) to associate with this certificate')
+ ->addOption('label', '', InputOption::VALUE_OPTIONAL, 'The label for this certificate. Required for standard certificates. Optional for legacy certificates', 'My certificate')
+ ->acceptEnvironmentId();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $environment = $this->determineEnvironment($input, $output, true, true);
+ $certificate = $input->getArgument('certificate');
+ $privateKey = $input->getArgument('private-key');
+ $label = $this->determineOption('label');
+ $caCertificates = $this->determineOption('ca-certificates');
+ $csrId = (int) $this->determineOption('csr-id');
+ $legacy = $this->determineOption('legacy', false, null, null, 'false');
+ $legacy = filter_var($legacy, FILTER_VALIDATE_BOOLEAN);
+
+ $sslCertificates = new SslCertificates($acquiaCloudClient);
+ $response = $sslCertificates->create(
+ $environment->uuid,
+ $label,
+ $this->localMachineHelper->readFile($certificate),
+ $this->localMachineHelper->readFile($privateKey),
+ $caCertificates ? $this->localMachineHelper->readFile($caCertificates) : null,
+ $csrId,
+ $legacy
+ );
+ $notificationUuid = CommandBase::getNotificationUuidFromResponse($response);
+ $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, 'Installing certificate');
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Env/EnvCopyCronCommand.php b/src/Command/Env/EnvCopyCronCommand.php
index 1f018d268..d6b6923a8 100644
--- a/src/Command/Env/EnvCopyCronCommand.php
+++ b/src/Command/Env/EnvCopyCronCommand.php
@@ -1,6 +1,6 @@
addArgument('source_env', InputArgument::REQUIRED, 'Alias of the source environment in the format `app-name.env` or the environment uuid')
- ->addArgument('dest_env', InputArgument::REQUIRED, 'Alias of the destination environment in the format `app-name.env` or the environment uuid')
- ->addUsage(' ')
- ->addUsage('myapp.dev myapp.prod')
- ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470 efgh1234-1111-2222-3333-0e02b2c3d470');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- // If both source and destination env inputs are same.
- if ($input->getArgument('source_env') === $input->getArgument('dest_env')) {
- $this->io->error('The source and destination environments can not be same.');
- return 1;
+final class EnvCopyCronCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('source_env', InputArgument::REQUIRED, 'Alias of the source environment in the format `app-name.env` or the environment uuid')
+ ->addArgument('dest_env', InputArgument::REQUIRED, 'Alias of the destination environment in the format `app-name.env` or the environment uuid')
+ ->addUsage(' ')
+ ->addUsage('myapp.dev myapp.prod')
+ ->addUsage('abcd1234-1111-2222-3333-0e02b2c3d470 efgh1234-1111-2222-3333-0e02b2c3d470');
}
- // Get source env alias.
- $this->convertEnvironmentAliasToUuid($input, 'source_env');
- $sourceEnvId = $input->getArgument('source_env');
-
- // Get destination env alias.
- $this->convertEnvironmentAliasToUuid($input, 'dest_env');
- $destEnvId = $input->getArgument('dest_env');
-
- // Get the cron resource.
- $cronResource = new Crons($this->cloudApiClientService->getClient());
- $sourceEnvCronList = $cronResource->getAll($sourceEnvId);
-
- // Ask for confirmation before starting the copy.
- $answer = $this->io->confirm('Are you sure you\'d like to copy the cron jobs from ' . $sourceEnvId . ' to ' . $destEnvId . '?');
- if (!$answer) {
- return Command::SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ // If both source and destination env inputs are same.
+ if ($input->getArgument('source_env') === $input->getArgument('dest_env')) {
+ $this->io->error('The source and destination environments can not be same.');
+ return 1;
+ }
- $onlySystemCrons = TRUE;
- foreach ($sourceEnvCronList as $cron) {
- if (!$cron->flags->system) {
- $onlySystemCrons = FALSE;
- }
- }
+ // Get source env alias.
+ $this->convertEnvironmentAliasToUuid($input, 'source_env');
+ $sourceEnvId = $input->getArgument('source_env');
- // If source environment doesn't have any cron job or only
- // has system crons.
- if ($onlySystemCrons || $sourceEnvCronList->count() === 0) {
- $this->io->error('There are no cron jobs in the source environment for copying.');
- return 1;
- }
+ // Get destination env alias.
+ $this->convertEnvironmentAliasToUuid($input, 'dest_env');
+ $destEnvId = $input->getArgument('dest_env');
- foreach ($sourceEnvCronList as $cron) {
- // We don't copy the system cron as those should already be there
- // when environment is provisioned.
- if (!$cron->flags->system) {
- $cronFrequency = implode(' ', [
- $cron->minute,
- $cron->hour,
- $cron->dayMonth,
- $cron->month,
- $cron->dayWeek,
- ]);
+ // Get the cron resource.
+ $cronResource = new Crons($this->cloudApiClientService->getClient());
+ $sourceEnvCronList = $cronResource->getAll($sourceEnvId);
- $this->io->info('Copying the cron task "' . $cron->label . '" from ' . $sourceEnvId . ' to ' . $destEnvId);
- try {
- // Copying the cron on destination environment.
- $cronResource->create(
- $destEnvId,
- $cron->command,
- $cronFrequency,
- $cron->label,
- );
+ // Ask for confirmation before starting the copy.
+ $answer = $this->io->confirm('Are you sure you\'d like to copy the cron jobs from ' . $sourceEnvId . ' to ' . $destEnvId . '?');
+ if (!$answer) {
+ return Command::SUCCESS;
+ }
+ $onlySystemCrons = true;
+ foreach ($sourceEnvCronList as $cron) {
+ if (!$cron->flags->system) {
+ $onlySystemCrons = false;
+ }
}
- catch (Exception $e) {
- $this->io->error('There was some error while copying the cron task "' . $cron->label . '"');
- // Log the error for debugging purpose.
- $this->logger->debug('Error @error while copying the cron task @cron from @source env to @dest env', [
- '@cron' => $cron->label,
- '@dest' => $destEnvId,
- '@error' => $e->getMessage(),
- '@source' => $sourceEnvId,
- ]);
- return 1;
+
+ // If source environment doesn't have any cron job or only
+ // has system crons.
+ if ($onlySystemCrons || $sourceEnvCronList->count() === 0) {
+ $this->io->error('There are no cron jobs in the source environment for copying.');
+ return 1;
}
- }
- }
- $this->io->success('Cron task copy is completed.');
- return Command::SUCCESS;
- }
+ foreach ($sourceEnvCronList as $cron) {
+ // We don't copy the system cron as those should already be there
+ // when environment is provisioned.
+ if (!$cron->flags->system) {
+ $cronFrequency = implode(' ', [
+ $cron->minute,
+ $cron->hour,
+ $cron->dayMonth,
+ $cron->month,
+ $cron->dayWeek,
+ ]);
+
+ $this->io->info('Copying the cron task "' . $cron->label . '" from ' . $sourceEnvId . ' to ' . $destEnvId);
+ try {
+ // Copying the cron on destination environment.
+ $cronResource->create(
+ $destEnvId,
+ $cron->command,
+ $cronFrequency,
+ $cron->label,
+ );
+ } catch (Exception $e) {
+ $this->io->error('There was some error while copying the cron task "' . $cron->label . '"');
+ // Log the error for debugging purpose.
+ $this->logger->debug('Error @error while copying the cron task @cron from @source env to @dest env', [
+ '@cron' => $cron->label,
+ '@dest' => $destEnvId,
+ '@error' => $e->getMessage(),
+ '@source' => $sourceEnvId,
+ ]);
+ return 1;
+ }
+ }
+ }
+ $this->io->success('Cron task copy is completed.');
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Env/EnvCreateCommand.php b/src/Command/Env/EnvCreateCommand.php
index ac22d29fe..3eb49463c 100644
--- a/src/Command/Env/EnvCreateCommand.php
+++ b/src/Command/Env/EnvCreateCommand.php
@@ -1,6 +1,6 @@
addArgument('label', InputArgument::REQUIRED, 'The label of the new environment');
- $this->addArgument('branch', InputArgument::OPTIONAL, 'The vcs path (git branch name) to deploy to the new environment');
- $this->acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->output = $output;
- $cloudAppUuid = $this->determineCloudApplication(TRUE);
- $label = $input->getArgument('label');
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $environmentsResource = new Environments($acquiaCloudClient);
- $this->checklist = new Checklist($output);
+ protected function configure(): void
+ {
+ $this->addArgument('label', InputArgument::REQUIRED, 'The label of the new environment');
+ $this->addArgument('branch', InputArgument::OPTIONAL, 'The vcs path (git branch name) to deploy to the new environment');
+ $this->acceptApplicationUuid();
+ }
- $this->validateLabel($environmentsResource, $cloudAppUuid, $label);
- $branch = $this->getBranch($acquiaCloudClient, $cloudAppUuid, $input);
- $databaseNames = $this->getDatabaseNames($acquiaCloudClient, $cloudAppUuid);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->output = $output;
+ $cloudAppUuid = $this->determineCloudApplication(true);
+ $label = $input->getArgument('label');
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $environmentsResource = new Environments($acquiaCloudClient);
+ $this->checklist = new Checklist($output);
- $this->checklist->addItem("Initiating environment creation");
- $response = $environmentsResource->create($cloudAppUuid, $label, $branch, $databaseNames);
- $notificationUuid = CommandBase::getNotificationUuidFromResponse($response);
- $this->checklist->completePreviousItem();
+ $this->validateLabel($environmentsResource, $cloudAppUuid, $label);
+ $branch = $this->getBranch($acquiaCloudClient, $cloudAppUuid, $input);
+ $databaseNames = $this->getDatabaseNames($acquiaCloudClient, $cloudAppUuid);
- $success = function () use ($environmentsResource, $cloudAppUuid, $label): void {
- $environments = $environmentsResource->getAll($cloudAppUuid);
- foreach ($environments as $environment) {
- if ($environment->label === $label) {
- break;
- }
- }
- if (isset($environment)) {
- $this->output->writeln([
- '',
- "Your CDE URL: domains[0]}>{$environment->domains[0]}>",
- ]);
- }
- };
- $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, "Waiting for the environment to be ready. This usually takes 2 - 15 minutes.", $success);
+ $this->checklist->addItem("Initiating environment creation");
+ $response = $environmentsResource->create($cloudAppUuid, $label, $branch, $databaseNames);
+ $notificationUuid = CommandBase::getNotificationUuidFromResponse($response);
+ $this->checklist->completePreviousItem();
- return Command::SUCCESS;
- }
+ $success = function () use ($environmentsResource, $cloudAppUuid, $label): void {
+ $environments = $environmentsResource->getAll($cloudAppUuid);
+ foreach ($environments as $environment) {
+ if ($environment->label === $label) {
+ break;
+ }
+ }
+ if (isset($environment)) {
+ $this->output->writeln([
+ '',
+ "Your CDE URL: domains[0]}>{$environment->domains[0]}>",
+ ]);
+ }
+ };
+ $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, "Waiting for the environment to be ready. This usually takes 2 - 15 minutes.", $success);
- private function validateLabel(Environments $environmentsResource, string $cloudAppUuid, string $title): void {
- $this->checklist->addItem("Checking to see that label is unique");
- /** @var \AcquiaCloudApi\Response\EnvironmentResponse[] $environments */
- $environments = $environmentsResource->getAll($cloudAppUuid);
- foreach ($environments as $environment) {
- if ($environment->label === $title) {
- throw new AcquiaCliException("An environment named $title already exists.");
- }
+ return Command::SUCCESS;
}
- $this->checklist->completePreviousItem();
- }
- private function getBranch(Client $acquiaCloudClient, ?string $cloudAppUuid, InputInterface $input): string {
- $branchesAndTags = $acquiaCloudClient->request('get', "/applications/$cloudAppUuid/code");
- if ($input->getArgument('branch')) {
- $branch = $input->getArgument('branch');
- if (!in_array($branch, array_column($branchesAndTags, 'name'), TRUE)) {
- throw new AcquiaCliException("There is no branch or tag with the name $branch on the remote VCS.", );
- }
- return $branch;
+ private function validateLabel(Environments $environmentsResource, string $cloudAppUuid, string $title): void
+ {
+ $this->checklist->addItem("Checking to see that label is unique");
+ /** @var \AcquiaCloudApi\Response\EnvironmentResponse[] $environments */
+ $environments = $environmentsResource->getAll($cloudAppUuid);
+ foreach ($environments as $environment) {
+ if ($environment->label === $title) {
+ throw new AcquiaCliException("An environment named $title already exists.");
+ }
+ }
+ $this->checklist->completePreviousItem();
}
- $branchOrTag = $this->promptChooseFromObjectsOrArrays($branchesAndTags, 'name', 'name', "Choose a branch or tag to deploy to the new environment");
- return $branchOrTag->name;
- }
- /**
- * @return array
- */
- private function getDatabaseNames(Client $acquiaCloudClient, ?string $cloudAppUuid): array {
- $this->checklist->addItem("Determining default database");
- $databasesResource = new Databases($acquiaCloudClient);
- $databases = $databasesResource->getNames($cloudAppUuid);
- $databaseNames = [];
- foreach ($databases as $database) {
- $databaseNames[] = $database->name;
+ private function getBranch(Client $acquiaCloudClient, ?string $cloudAppUuid, InputInterface $input): string
+ {
+ $branchesAndTags = $acquiaCloudClient->request('get', "/applications/$cloudAppUuid/code");
+ if ($input->getArgument('branch')) {
+ $branch = $input->getArgument('branch');
+ if (!in_array($branch, array_column($branchesAndTags, 'name'), true)) {
+ throw new AcquiaCliException("There is no branch or tag with the name $branch on the remote VCS.",);
+ }
+ return $branch;
+ }
+ $branchOrTag = $this->promptChooseFromObjectsOrArrays($branchesAndTags, 'name', 'name', "Choose a branch or tag to deploy to the new environment");
+ return $branchOrTag->name;
}
- $this->checklist->completePreviousItem();
- return $databaseNames;
- }
+ /**
+ * @return array
+ */
+ private function getDatabaseNames(Client $acquiaCloudClient, ?string $cloudAppUuid): array
+ {
+ $this->checklist->addItem("Determining default database");
+ $databasesResource = new Databases($acquiaCloudClient);
+ $databases = $databasesResource->getNames($cloudAppUuid);
+ $databaseNames = [];
+ foreach ($databases as $database) {
+ $databaseNames[] = $database->name;
+ }
+ $this->checklist->completePreviousItem();
+ return $databaseNames;
+ }
}
diff --git a/src/Command/Env/EnvDeleteCommand.php b/src/Command/Env/EnvDeleteCommand.php
index d68dafc04..25719c9c1 100644
--- a/src/Command/Env/EnvDeleteCommand.php
+++ b/src/Command/Env/EnvDeleteCommand.php
@@ -1,6 +1,6 @@
acceptEnvironmentId();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->output = $output;
- $cloudAppUuid = $this->determineCloudApplication(TRUE);
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $environmentsResource = new Environments($acquiaCloudClient);
- $environment = $this->determineEnvironmentCde($environmentsResource, $cloudAppUuid);
- $environmentsResource->delete($environment->uuid);
-
- $this->io->success([
- "The {$environment->label} environment is being deleted",
- ]);
-
- return Command::SUCCESS;
- }
-
- private function determineEnvironmentCde(Environments $environmentsResource, string $cloudAppUuid): EnvironmentResponse {
- if ($this->input->getArgument('environmentId')) {
- // @todo Validate.
- $environmentId = $this->input->getArgument('environmentId');
- return $environmentsResource->get($environmentId);
+final class EnvDeleteCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this->acceptEnvironmentId();
}
- $environments = $environmentsResource->getAll($cloudAppUuid);
- $cdes = [];
- foreach ($environments as $environment) {
- if ($environment->flags->cde) {
- $cdes[] = $environment;
- }
- }
- if (!$cdes) {
- throw new AcquiaCliException('There are no existing CDEs for Application ' . $cloudAppUuid);
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->output = $output;
+ $cloudAppUuid = $this->determineCloudApplication(true);
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $environmentsResource = new Environments($acquiaCloudClient);
+ $environment = $this->determineEnvironmentCde($environmentsResource, $cloudAppUuid);
+ $environmentsResource->delete($environment->uuid);
+
+ $this->io->success([
+ "The {$environment->label} environment is being deleted",
+ ]);
+
+ return Command::SUCCESS;
}
- return $this->promptChooseFromObjectsOrArrays($cdes, 'uuid', 'label', "Which Continuous Delivery Environment (CDE) do you want to delete?");
- }
+ private function determineEnvironmentCde(Environments $environmentsResource, string $cloudAppUuid): EnvironmentResponse
+ {
+ if ($this->input->getArgument('environmentId')) {
+ // @todo Validate.
+ $environmentId = $this->input->getArgument('environmentId');
+ return $environmentsResource->get($environmentId);
+ }
+ $environments = $environmentsResource->getAll($cloudAppUuid);
+ $cdes = [];
+ foreach ($environments as $environment) {
+ if ($environment->flags->cde) {
+ $cdes[] = $environment;
+ }
+ }
+ if (!$cdes) {
+ throw new AcquiaCliException('There are no existing CDEs for Application ' . $cloudAppUuid);
+ }
+ return $this->promptChooseFromObjectsOrArrays($cdes, 'uuid', 'label', "Which Continuous Delivery Environment (CDE) do you want to delete?");
+ }
}
diff --git a/src/Command/Env/EnvMirrorCommand.php b/src/Command/Env/EnvMirrorCommand.php
index 7a2038717..e3860ff7e 100644
--- a/src/Command/Env/EnvMirrorCommand.php
+++ b/src/Command/Env/EnvMirrorCommand.php
@@ -1,6 +1,6 @@
addArgument('source-environment', InputArgument::REQUIRED, 'The Cloud Platform source environment ID or alias')
- ->addUsage('[]')
- ->addUsage('myapp.dev')
- ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470');
- $this->addArgument('destination-environment', InputArgument::REQUIRED, 'The Cloud Platform destination environment ID or alias')
- ->addUsage('[]')
- ->addUsage('myapp.dev')
- ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470');
- $this->addOption('no-code', 'c');
- $this->addOption('no-databases', 'd');
- $this->addOption('no-files', 'f');
- $this->addOption('no-config', 'p');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->checklist = new Checklist($output);
- $outputCallback = $this->getOutputCallback($output, $this->checklist);
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $environmentsResource = new Environments($acquiaCloudClient);
- $sourceEnvironmentUuid = $input->getArgument('source-environment');
- $destinationEnvironmentUuid = $input->getArgument('destination-environment');
-
- $this->checklist->addItem("Fetching information about source environment");
- $sourceEnvironment = $environmentsResource->get($sourceEnvironmentUuid);
- $this->checklist->completePreviousItem();
-
- $this->checklist->addItem("Fetching information about destination environment");
- $destinationEnvironment = $environmentsResource->get($destinationEnvironmentUuid);
- $this->checklist->completePreviousItem();
-
- $answer = $this->io->confirm("Are you sure that you want to overwrite everything on {$destinationEnvironment->label} ({$destinationEnvironment->name}) and replace it with source data from {$sourceEnvironment->label} ({$sourceEnvironment->name})");
- if (!$answer) {
- return 1;
+final class EnvMirrorCommand extends CommandBase
+{
+ private Checklist $checklist;
+
+ protected function configure(): void
+ {
+ $this->addArgument('source-environment', InputArgument::REQUIRED, 'The Cloud Platform source environment ID or alias')
+ ->addUsage('[]')
+ ->addUsage('myapp.dev')
+ ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470');
+ $this->addArgument('destination-environment', InputArgument::REQUIRED, 'The Cloud Platform destination environment ID or alias')
+ ->addUsage('[]')
+ ->addUsage('myapp.dev')
+ ->addUsage('12345-abcd1234-1111-2222-3333-0e02b2c3d470');
+ $this->addOption('no-code', 'c');
+ $this->addOption('no-databases', 'd');
+ $this->addOption('no-files', 'f');
+ $this->addOption('no-config', 'p');
}
- if (!$input->getOption('no-code')) {
- $codeCopyResponse = $this->mirrorCode($acquiaCloudClient, $destinationEnvironmentUuid, $sourceEnvironment, $outputCallback);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->checklist = new Checklist($output);
+ $outputCallback = $this->getOutputCallback($output, $this->checklist);
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $environmentsResource = new Environments($acquiaCloudClient);
+ $sourceEnvironmentUuid = $input->getArgument('source-environment');
+ $destinationEnvironmentUuid = $input->getArgument('destination-environment');
+
+ $this->checklist->addItem("Fetching information about source environment");
+ $sourceEnvironment = $environmentsResource->get($sourceEnvironmentUuid);
+ $this->checklist->completePreviousItem();
+
+ $this->checklist->addItem("Fetching information about destination environment");
+ $destinationEnvironment = $environmentsResource->get($destinationEnvironmentUuid);
+ $this->checklist->completePreviousItem();
+
+ $answer = $this->io->confirm("Are you sure that you want to overwrite everything on {$destinationEnvironment->label} ({$destinationEnvironment->name}) and replace it with source data from {$sourceEnvironment->label} ({$sourceEnvironment->name})");
+ if (!$answer) {
+ return 1;
+ }
+
+ if (!$input->getOption('no-code')) {
+ $codeCopyResponse = $this->mirrorCode($acquiaCloudClient, $destinationEnvironmentUuid, $sourceEnvironment, $outputCallback);
+ }
+
+ if (!$input->getOption('no-databases')) {
+ $dbCopyResponse = $this->mirrorDatabase($acquiaCloudClient, $sourceEnvironmentUuid, $destinationEnvironmentUuid, $outputCallback);
+ }
+
+ if (!$input->getOption('no-files')) {
+ $filesCopyResponse = $this->mirrorFiles($environmentsResource, $sourceEnvironmentUuid, $destinationEnvironmentUuid);
+ }
+
+ if (!$input->getOption('no-config')) {
+ $configCopyResponse = $this->mirrorConfig($sourceEnvironment, $destinationEnvironment, $environmentsResource, $destinationEnvironmentUuid, $outputCallback);
+ }
+
+ if (isset($codeCopyResponse)) {
+ $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete');
+ }
+ if (isset($dbCopyResponse)) {
+ $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete');
+ }
+ if (isset($filesCopyResponse)) {
+ $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete');
+ }
+ if (isset($configCopyResponse)) {
+ $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete');
+ }
+
+ $this->io->success([
+ "Done! {$destinationEnvironment->label} now matches {$sourceEnvironment->label}",
+ "You can visit it here:",
+ "https://" . $destinationEnvironment->domains[0],
+ ]);
+
+ return Command::SUCCESS;
}
- if (!$input->getOption('no-databases')) {
- $dbCopyResponse = $this->mirrorDatabase($acquiaCloudClient, $sourceEnvironmentUuid, $destinationEnvironmentUuid, $outputCallback);
+ private function getDefaultDatabase(array $databases): ?object
+ {
+ foreach ($databases as $database) {
+ if ($database->flags->default) {
+ return $database;
+ }
+ }
+ return null;
}
- if (!$input->getOption('no-files')) {
- $filesCopyResponse = $this->mirrorFiles($environmentsResource, $sourceEnvironmentUuid, $destinationEnvironmentUuid);
+ private function mirrorDatabase(Client $acquiaCloudClient, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse
+ {
+ $this->checklist->addItem("Initiating database copy");
+ $outputCallback('out', "Getting a list of databases");
+ $databasesResource = new Databases($acquiaCloudClient);
+ $databases = $acquiaCloudClient->request('get', "/environments/$sourceEnvironmentUuid/databases");
+ $defaultDatabase = $this->getDefaultDatabase($databases);
+ $outputCallback('out', "Copying {$defaultDatabase->name}");
+
+ // @todo Create database if its missing.
+ $dbCopyResponse = $databasesResource->copy($sourceEnvironmentUuid, $defaultDatabase->name, $destinationEnvironmentUuid);
+ $this->checklist->completePreviousItem();
+ return $dbCopyResponse;
}
- if (!$input->getOption('no-config')) {
- $configCopyResponse = $this->mirrorConfig($sourceEnvironment, $destinationEnvironment, $environmentsResource, $destinationEnvironmentUuid, $outputCallback);
+ private function mirrorCode(Client $acquiaCloudClient, mixed $destinationEnvironmentUuid, EnvironmentResponse $sourceEnvironment, callable $outputCallback): mixed
+ {
+ $this->checklist->addItem("Initiating code switch");
+ $outputCallback('out', "Switching to {$sourceEnvironment->vcs->path}");
+ $codeCopyResponse = $acquiaCloudClient->request('post', "/environments/$destinationEnvironmentUuid/code/actions/switch", [
+ 'form_params' => [
+ 'branch' => $sourceEnvironment->vcs->path,
+ ],
+ ]);
+ $codeCopyResponse->links = $codeCopyResponse->_links;
+ $this->checklist->completePreviousItem();
+ return $codeCopyResponse;
}
- if (isset($codeCopyResponse)) {
- $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($codeCopyResponse), 'Waiting for code copy to complete');
- }
- if (isset($dbCopyResponse)) {
- $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($dbCopyResponse), 'Waiting for database copy to complete');
- }
- if (isset($filesCopyResponse)) {
- $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($filesCopyResponse), 'Waiting for files copy to complete');
+ private function mirrorFiles(Environments $environmentsResource, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid): OperationResponse
+ {
+ $this->checklist->addItem("Initiating files copy");
+ $filesCopyResponse = $environmentsResource->copyFiles($sourceEnvironmentUuid, $destinationEnvironmentUuid);
+ $this->checklist->completePreviousItem();
+ return $filesCopyResponse;
}
- if (isset($configCopyResponse)) {
- $this->waitForNotificationToComplete($acquiaCloudClient, CommandBase::getNotificationUuidFromResponse($configCopyResponse), 'Waiting for config copy to complete');
- }
-
- $this->io->success([
- "Done! {$destinationEnvironment->label} now matches {$sourceEnvironment->label}",
- "You can visit it here:",
- "https://" . $destinationEnvironment->domains[0],
- ]);
- return Command::SUCCESS;
- }
-
- private function getDefaultDatabase(array $databases): ?object {
- foreach ($databases as $database) {
- if ($database->flags->default) {
- return $database;
- }
- }
- return NULL;
- }
-
- private function mirrorDatabase(Client $acquiaCloudClient, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse {
- $this->checklist->addItem("Initiating database copy");
- $outputCallback('out', "Getting a list of databases");
- $databasesResource = new Databases($acquiaCloudClient);
- $databases = $acquiaCloudClient->request('get', "/environments/$sourceEnvironmentUuid/databases");
- $defaultDatabase = $this->getDefaultDatabase($databases);
- $outputCallback('out', "Copying {$defaultDatabase->name}");
-
- // @todo Create database if its missing.
- $dbCopyResponse = $databasesResource->copy($sourceEnvironmentUuid, $defaultDatabase->name, $destinationEnvironmentUuid);
- $this->checklist->completePreviousItem();
- return $dbCopyResponse;
- }
-
- private function mirrorCode(Client $acquiaCloudClient, mixed $destinationEnvironmentUuid, EnvironmentResponse $sourceEnvironment, callable $outputCallback): mixed {
- $this->checklist->addItem("Initiating code switch");
- $outputCallback('out', "Switching to {$sourceEnvironment->vcs->path}");
- $codeCopyResponse = $acquiaCloudClient->request('post', "/environments/$destinationEnvironmentUuid/code/actions/switch", [
- 'form_params' => [
- 'branch' => $sourceEnvironment->vcs->path,
- ],
- ]);
- $codeCopyResponse->links = $codeCopyResponse->_links;
- $this->checklist->completePreviousItem();
- return $codeCopyResponse;
- }
-
- private function mirrorFiles(Environments $environmentsResource, mixed $sourceEnvironmentUuid, mixed $destinationEnvironmentUuid): OperationResponse {
- $this->checklist->addItem("Initiating files copy");
- $filesCopyResponse = $environmentsResource->copyFiles($sourceEnvironmentUuid, $destinationEnvironmentUuid);
- $this->checklist->completePreviousItem();
- return $filesCopyResponse;
- }
-
- private function mirrorConfig(EnvironmentResponse $sourceEnvironment, EnvironmentResponse $destinationEnvironment, Environments $environmentsResource, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse {
- $this->checklist->addItem("Initiating config copy");
- $outputCallback('out', "Copying PHP version, acpu memory limit, etc.");
- $config = (array) $sourceEnvironment->configuration->php;
- $config['apcu'] = max(32, $sourceEnvironment->configuration->php->apcu);
- if ($config['version'] == $destinationEnvironment->configuration->php->version) {
- unset($config['version']);
+ private function mirrorConfig(EnvironmentResponse $sourceEnvironment, EnvironmentResponse $destinationEnvironment, Environments $environmentsResource, mixed $destinationEnvironmentUuid, callable $outputCallback): OperationResponse
+ {
+ $this->checklist->addItem("Initiating config copy");
+ $outputCallback('out', "Copying PHP version, acpu memory limit, etc.");
+ $config = (array) $sourceEnvironment->configuration->php;
+ $config['apcu'] = max(32, $sourceEnvironment->configuration->php->apcu);
+ if ($config['version'] == $destinationEnvironment->configuration->php->version) {
+ unset($config['version']);
+ }
+ unset($config['memcached_limit']);
+ $configCopyResponse = $environmentsResource->update($destinationEnvironmentUuid, $config);
+ $this->checklist->completePreviousItem();
+ return $configCopyResponse;
}
- unset($config['memcached_limit']);
- $configCopyResponse = $environmentsResource->update($destinationEnvironmentUuid, $config);
- $this->checklist->completePreviousItem();
- return $configCopyResponse;
- }
-
}
diff --git a/src/Command/HelloWorldCommand.php b/src/Command/HelloWorldCommand.php
index c3409f6cc..d51c07ec2 100644
--- a/src/Command/HelloWorldCommand.php
+++ b/src/Command/HelloWorldCommand.php
@@ -1,6 +1,6 @@
io->success('Hello world!');
-
- return Command::SUCCESS;
- }
+#[AsCommand(name: 'hello-world', description: 'Test command used for asserting core functionality', hidden: true)]
+final class HelloWorldCommand extends CommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->io->success('Hello world!');
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ide/IdeCommandBase.php b/src/Command/Ide/IdeCommandBase.php
index 3c07ab03d..f3204ccc3 100644
--- a/src/Command/Ide/IdeCommandBase.php
+++ b/src/Command/Ide/IdeCommandBase.php
@@ -1,6 +1,6 @@
getAll($cloudApplicationUuid));
+ if (empty($ides)) {
+ throw new AcquiaCliException('No IDEs exist for this application.');
+ }
- protected function promptIdeChoice(
- string $questionText,
- Ides $idesResource,
- string $cloudApplicationUuid
- ): IdeResponse {
- $ides = iterator_to_array($idesResource->getAll($cloudApplicationUuid));
- if (empty($ides)) {
- throw new AcquiaCliException('No IDEs exist for this application.');
- }
+ $choices = [];
+ foreach ($ides as $ide) {
+ $choices[] = "$ide->label ($ide->uuid)";
+ }
+ $choice = $this->io->choice($questionText, $choices, $choices[0]);
+ $chosenEnvironmentIndex = array_search($choice, $choices, true);
- $choices = [];
- foreach ($ides as $ide) {
- $choices[] = "$ide->label ($ide->uuid)";
+ return $ides[$chosenEnvironmentIndex];
}
- $choice = $this->io->choice($questionText, $choices, $choices[0]);
- $chosenEnvironmentIndex = array_search($choice, $choices, TRUE);
-
- return $ides[$chosenEnvironmentIndex];
- }
- /**
- * Start service inside IDE.
- */
- protected function startService(string $service): void {
- $process = $this->localMachineHelper->execute([
- 'supervisorctl',
- 'start',
- $service,
- ], NULL, NULL, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to start ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]);
+ /**
+ * Start service inside IDE.
+ */
+ protected function startService(string $service): void
+ {
+ $process = $this->localMachineHelper->execute([
+ 'supervisorctl',
+ 'start',
+ $service,
+ ], null, null, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to start ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]);
+ }
}
- }
- /**
- * Stop service inside IDE.
- */
- protected function stopService(string $service): void {
- $process = $this->localMachineHelper->execute([
- 'supervisorctl',
- 'stop',
- $service,
- ], NULL, NULL, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to stop ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]);
+ /**
+ * Stop service inside IDE.
+ */
+ protected function stopService(string $service): void
+ {
+ $process = $this->localMachineHelper->execute([
+ 'supervisorctl',
+ 'stop',
+ $service,
+ ], null, null, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to stop ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]);
+ }
}
- }
- /**
- * Restart service inside IDE.
- */
- protected function restartService(string $service): void {
- $process = $this->localMachineHelper->execute([
- 'supervisorctl',
- 'restart',
- $service,
- ], NULL, NULL, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to restart ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]);
+ /**
+ * Restart service inside IDE.
+ */
+ protected function restartService(string $service): void
+ {
+ $process = $this->localMachineHelper->execute([
+ 'supervisorctl',
+ 'restart',
+ $service,
+ ], null, null, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to restart ' . $service . ' in the IDE: {error}', ['error' => $process->getErrorOutput()]);
+ }
}
- }
-
- public function setXdebugIniFilepath(string $filePath): void {
- $this->xdebugIniFilepath = $filePath;
- }
- protected function getXdebugIniFilePath(): string {
- return $this->xdebugIniFilepath;
- }
+ public function setXdebugIniFilepath(string $filePath): void
+ {
+ $this->xdebugIniFilepath = $filePath;
+ }
+ protected function getXdebugIniFilePath(): string
+ {
+ return $this->xdebugIniFilepath;
+ }
}
diff --git a/src/Command/Ide/IdeCreateCommand.php b/src/Command/Ide/IdeCreateCommand.php
index 932bd073c..2c106e5cd 100644
--- a/src/Command/Ide/IdeCreateCommand.php
+++ b/src/Command/Ide/IdeCreateCommand.php
@@ -1,6 +1,6 @@
localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger);
- }
-
- protected function configure(): void {
- $this->acceptApplicationUuid();
- $this->addOption('label', NULL, InputOption::VALUE_REQUIRED, 'The label for the IDE');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $cloudApplicationUuid = $this->determineCloudApplication();
- $checklist = new Checklist($output);
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $accountResource = new Account($acquiaCloudClient);
- $account = $accountResource->get();
- $default = "$account->first_name $account->last_name's IDE";
- $ideLabel = $this->determineOption('label', FALSE, $this->validateIdeLabel(...), NULL, $default);
+final class IdeCreateCommand extends IdeCommandBase
+{
+ private IdeResponse $ide;
+
+ public function __construct(
+ public LocalMachineHelper $localMachineHelper,
+ protected CloudDataStore $datastoreCloud,
+ protected AcquiaCliDatastore $datastoreAcli,
+ protected ApiCredentialsInterface $cloudCredentials,
+ protected TelemetryHelper $telemetryHelper,
+ protected string $projectDir,
+ protected ClientService $cloudApiClientService,
+ public SshHelper $sshHelper,
+ protected string $sshDir,
+ LoggerInterface $logger,
+ protected Client $httpClient
+ ) {
+ parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger);
+ }
- // Create it.
- $checklist->addItem('Creating your Cloud IDE');
- $idesResource = new Ides($acquiaCloudClient);
- $response = $idesResource->create($cloudApplicationUuid, $ideLabel);
- $checklist->completePreviousItem();
+ protected function configure(): void
+ {
+ $this->acceptApplicationUuid();
+ $this->addOption('label', null, InputOption::VALUE_REQUIRED, 'The label for the IDE');
+ }
- // Get IDE info.
- $checklist->addItem('Getting IDE information');
- $this->ide = $this->getIdeFromResponse($response, $acquiaCloudClient);
- $ideUrl = $this->ide->links->ide->href;
- $checklist->completePreviousItem();
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $cloudApplicationUuid = $this->determineCloudApplication();
+ $checklist = new Checklist($output);
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $accountResource = new Account($acquiaCloudClient);
+ $account = $accountResource->get();
+ $default = "$account->first_name $account->last_name's IDE";
+ $ideLabel = $this->determineOption('label', false, $this->validateIdeLabel(...), null, $default);
+
+ // Create it.
+ $checklist->addItem('Creating your Cloud IDE');
+ $idesResource = new Ides($acquiaCloudClient);
+ $response = $idesResource->create($cloudApplicationUuid, $ideLabel);
+ $checklist->completePreviousItem();
+
+ // Get IDE info.
+ $checklist->addItem('Getting IDE information');
+ $this->ide = $this->getIdeFromResponse($response, $acquiaCloudClient);
+ $ideUrl = $this->ide->links->ide->href;
+ $checklist->completePreviousItem();
+
+ // Wait!
+ return $this->waitForDnsPropagation($ideUrl);
+ }
- // Wait!
- return $this->waitForDnsPropagation($ideUrl);
- }
+ /**
+ * Keep this public since it's used as a callback and static analysis tools
+ * think it's unused.
+ *
+ * @todo use first-class callable syntax instead once we upgrade to PHP 8.1
+ * @see https://www.php.net/manual/en/functions.first_class_callable_syntax.php
+ */
+ public function validateIdeLabel(string $label): string
+ {
+ $violations = Validation::createValidator()->validate($label, [
+ new Regex(['pattern' => '/^[\w\' ]+$/', 'message' => 'Use only letters, numbers, and spaces']),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ return $label;
+ }
- /**
- * Keep this public since it's used as a callback and static analysis tools
- * think it's unused.
- *
- * @todo use first-class callable syntax instead once we upgrade to PHP 8.1
- * @see https://www.php.net/manual/en/functions.first_class_callable_syntax.php
- */
- public function validateIdeLabel(string $label): string {
- $violations = Validation::createValidator()->validate($label, [
- new Regex(['pattern' => '/^[\w\' ]+$/', 'message' => 'Use only letters, numbers, and spaces']),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+ private function waitForDnsPropagation(string $ideUrl): int
+ {
+ $ideCreated = false;
+ $checkIdeStatus = function () use (&$ideCreated, $ideUrl) {
+ // Ideally we'd set $ideUrl as the Guzzle base_url, but that requires creating a client factory.
+ // @see https://stackoverflow.com/questions/28277889/guzzlehttp-client-change-base-url-dynamically
+ $response = $this->httpClient->request('GET', "$ideUrl/health", ['http_errors' => false]);
+ // Mutating this will result in an infinite loop and timeout.
+ // @infection-ignore-all
+ if ($response->getStatusCode() === 200) {
+ $ideCreated = true;
+ }
+ return $ideCreated;
+ };
+ $doneCallback = function () use (&$ideCreated): void {
+ if ($ideCreated) {
+ $this->output->writeln('');
+ $this->output->writeln('Your IDE is ready!');
+ }
+ $this->writeIdeLinksToScreen();
+ };
+ $spinnerMessage = 'Waiting for the IDE to be ready. This usually takes 2 - 15 minutes.';
+ LoopHelper::getLoopy($this->output, $this->io, $spinnerMessage, $checkIdeStatus, $doneCallback);
+
+ return Command::SUCCESS;
}
- return $label;
- }
- private function waitForDnsPropagation(string $ideUrl): int {
- $ideCreated = FALSE;
- $checkIdeStatus = function () use (&$ideCreated, $ideUrl) {
- // Ideally we'd set $ideUrl as the Guzzle base_url, but that requires creating a client factory.
- // @see https://stackoverflow.com/questions/28277889/guzzlehttp-client-change-base-url-dynamically
- $response = $this->httpClient->request('GET', "$ideUrl/health", ['http_errors' => FALSE]);
- // Mutating this will result in an infinite loop and timeout.
- // @infection-ignore-all
- if ($response->getStatusCode() === 200) {
- $ideCreated = TRUE;
- }
- return $ideCreated;
- };
- $doneCallback = function () use (&$ideCreated): void {
- if ($ideCreated) {
+ /**
+ * Writes the IDE links to screen.
+ */
+ private function writeIdeLinksToScreen(): void
+ {
$this->output->writeln('');
- $this->output->writeln('Your IDE is ready!');
- }
- $this->writeIdeLinksToScreen();
- };
- $spinnerMessage = 'Waiting for the IDE to be ready. This usually takes 2 - 15 minutes.';
- LoopHelper::getLoopy($this->output, $this->io, $spinnerMessage, $checkIdeStatus, $doneCallback);
-
- return Command::SUCCESS;
- }
-
- /**
- * Writes the IDE links to screen.
- */
- private function writeIdeLinksToScreen(): void {
- $this->output->writeln('');
- $this->output->writeln("Your IDE URL: ide->links->ide->href}>{$this->ide->links->ide->href}>");
- $this->output->writeln("Your Drupal Site URL: ide->links->web->href}>{$this->ide->links->web->href}>");
- // @todo Prompt to open browser.
- }
-
- private function getIdeFromResponse(
- OperationResponse $response,
- \AcquiaCloudApi\Connector\Client $acquiaCloudClient
- ): IdeResponse {
- $cloudApiIdeUrl = $response->links->self->href;
- $urlParts = explode('/', $cloudApiIdeUrl);
- $ideUuid = end($urlParts);
- return (new Ides($acquiaCloudClient))->get($ideUuid);
- }
+ $this->output->writeln("Your IDE URL: ide->links->ide->href}>{$this->ide->links->ide->href}>");
+ $this->output->writeln("Your Drupal Site URL: ide->links->web->href}>{$this->ide->links->web->href}>");
+ // @todo Prompt to open browser.
+ }
+ private function getIdeFromResponse(
+ OperationResponse $response,
+ \AcquiaCloudApi\Connector\Client $acquiaCloudClient
+ ): IdeResponse {
+ $cloudApiIdeUrl = $response->links->self->href;
+ $urlParts = explode('/', $cloudApiIdeUrl);
+ $ideUuid = end($urlParts);
+ return (new Ides($acquiaCloudClient))->get($ideUuid);
+ }
}
diff --git a/src/Command/Ide/IdeDeleteCommand.php b/src/Command/Ide/IdeDeleteCommand.php
index e90ea2af4..b54a9aa88 100644
--- a/src/Command/Ide/IdeDeleteCommand.php
+++ b/src/Command/Ide/IdeDeleteCommand.php
@@ -1,6 +1,6 @@
acceptApplicationUuid();
- // @todo make this an argument
- $this->addOption('uuid', NULL, InputOption::VALUE_OPTIONAL, 'UUID of the IDE to delete');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $idesResource = new Ides($acquiaCloudClient);
-
- $ideUuid = $input->getOption('uuid');
- if ($ideUuid) {
- $ide = $idesResource->get($ideUuid);
- }
- else {
- $cloudApplicationUuid = $this->determineCloudApplication();
- $ide = $this->promptIdeChoice("Select the IDE you'd like to delete:", $idesResource, $cloudApplicationUuid);
- $answer = $this->io->confirm("Are you sure you want to delete $ide->label>");
- if (!$answer) {
- $this->io->writeln('Ok, never mind.');
- return Command::FAILURE;
- }
- }
- $response = $idesResource->delete($ide->uuid);
- $this->io->writeln($response->message);
-
- // Check to see if an SSH key for this IDE exists on Cloud.
- $cloudKey = $this->findIdeSshKeyOnCloud($ide->label, $ide->uuid);
- if ($cloudKey) {
- $answer = $this->io->confirm('Would you like to delete the SSH key associated with this IDE from your Cloud Platform account?');
- if ($answer) {
- $this->deleteSshKeyFromCloud($output, $cloudKey);
- }
+final class IdeDeleteCommand extends IdeCommandBase
+{
+ use SshCommandTrait;
+
+ protected function configure(): void
+ {
+ $this->acceptApplicationUuid();
+ // @todo make this an argument
+ $this->addOption('uuid', null, InputOption::VALUE_OPTIONAL, 'UUID of the IDE to delete');
}
- return Command::SUCCESS;
- }
-
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $idesResource = new Ides($acquiaCloudClient);
+
+ $ideUuid = $input->getOption('uuid');
+ if ($ideUuid) {
+ $ide = $idesResource->get($ideUuid);
+ } else {
+ $cloudApplicationUuid = $this->determineCloudApplication();
+ $ide = $this->promptIdeChoice("Select the IDE you'd like to delete:", $idesResource, $cloudApplicationUuid);
+ $answer = $this->io->confirm("Are you sure you want to delete $ide->label>");
+ if (!$answer) {
+ $this->io->writeln('Ok, never mind.');
+ return Command::FAILURE;
+ }
+ }
+ $response = $idesResource->delete($ide->uuid);
+ $this->io->writeln($response->message);
+
+ // Check to see if an SSH key for this IDE exists on Cloud.
+ $cloudKey = $this->findIdeSshKeyOnCloud($ide->label, $ide->uuid);
+ if ($cloudKey) {
+ $answer = $this->io->confirm('Would you like to delete the SSH key associated with this IDE from your Cloud Platform account?');
+ if ($answer) {
+ $this->deleteSshKeyFromCloud($output, $cloudKey);
+ }
+ }
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ide/IdeInfoCommand.php b/src/Command/Ide/IdeInfoCommand.php
index 026a4ddf7..9c075081a 100644
--- a/src/Command/Ide/IdeInfoCommand.php
+++ b/src/Command/Ide/IdeInfoCommand.php
@@ -1,6 +1,6 @@
acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $applicationUuid = $this->determineCloudApplication();
-
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $idesResource = new Ides($acquiaCloudClient);
-
- $ide = $this->promptIdeChoice("Select an IDE to get more information:", $idesResource, $applicationUuid);
- $response = $idesResource->get($ide->uuid);
- $this->io->definitionList(
- ['IDE property' => 'IDE value'],
- new TableSeparator(),
- ['UUID' => $response->uuid],
- ['Label' => $response->label],
- ['Owner name' => $response->owner->first_name . ' ' . $response->owner->last_name],
- ['Owner username' => $response->owner->username],
- ['Owner email' => $response->owner->mail],
- ['Cloud application' => $response->links->application->href],
- ['IDE URL' => $response->links->ide->href],
- ['Web URL' => $response->links->web->href]
- );
-
- return Command::SUCCESS;
- }
-
+final class IdeInfoCommand extends IdeCommandBase
+{
+ protected function configure(): void
+ {
+ $this->acceptApplicationUuid();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $applicationUuid = $this->determineCloudApplication();
+
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $idesResource = new Ides($acquiaCloudClient);
+
+ $ide = $this->promptIdeChoice("Select an IDE to get more information:", $idesResource, $applicationUuid);
+ $response = $idesResource->get($ide->uuid);
+ $this->io->definitionList(
+ ['IDE property' => 'IDE value'],
+ new TableSeparator(),
+ ['UUID' => $response->uuid],
+ ['Label' => $response->label],
+ ['Owner name' => $response->owner->first_name . ' ' . $response->owner->last_name],
+ ['Owner username' => $response->owner->username],
+ ['Owner email' => $response->owner->mail],
+ ['Cloud application' => $response->links->application->href],
+ ['IDE URL' => $response->links->ide->href],
+ ['Web URL' => $response->links->web->href]
+ );
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ide/IdeListCommand.php b/src/Command/Ide/IdeListCommand.php
index ea83d2214..dfa5e9bac 100644
--- a/src/Command/Ide/IdeListCommand.php
+++ b/src/Command/Ide/IdeListCommand.php
@@ -1,6 +1,6 @@
acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $applicationUuid = $this->determineCloudApplication();
-
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $idesResource = new Ides($acquiaCloudClient);
- $applicationIdes = $idesResource->getAll($applicationUuid);
-
- if ($applicationIdes->count()) {
- $table = new Table($output);
- $table->setStyle('borderless');
- $table->setHeaders(['IDEs']);
- foreach ($applicationIdes as $ide) {
- $table->addRows([
- ["{$ide->label} ({$ide->owner->mail})"],
- ["IDE URL: links->ide->href}>{$ide->links->ide->href}>"],
- ["Web URL: links->web->href}>{$ide->links->web->href}>"],
- new TableSeparator(),
- ]);
- }
- $table->render();
+final class IdeListCommand extends IdeCommandBase
+{
+ protected function configure(): void
+ {
+ $this->acceptApplicationUuid();
}
- else {
- $output->writeln('No IDE exists for this application.');
- }
-
- return Command::SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $applicationUuid = $this->determineCloudApplication();
+
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $idesResource = new Ides($acquiaCloudClient);
+ $applicationIdes = $idesResource->getAll($applicationUuid);
+
+ if ($applicationIdes->count()) {
+ $table = new Table($output);
+ $table->setStyle('borderless');
+ $table->setHeaders(['IDEs']);
+ foreach ($applicationIdes as $ide) {
+ $table->addRows([
+ ["{$ide->label} ({$ide->owner->mail})"],
+ ["IDE URL: links->ide->href}>{$ide->links->ide->href}>"],
+ ["Web URL: links->web->href}>{$ide->links->web->href}>"],
+ new TableSeparator(),
+ ]);
+ }
+ $table->render();
+ } else {
+ $output->writeln('No IDE exists for this application.');
+ }
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ide/IdeListMineCommand.php b/src/Command/Ide/IdeListMineCommand.php
index 7e1409b0f..cb7fb3f87 100644
--- a/src/Command/Ide/IdeListMineCommand.php
+++ b/src/Command/Ide/IdeListMineCommand.php
@@ -1,6 +1,6 @@
cloudApiClientService->getClient();
- $ides = new Ides($acquiaCloudClient);
- $accountIdes = $ides->getMine();
- $applicationResource = new Applications($acquiaCloudClient);
-
- if (count($accountIdes)) {
- $table = new Table($output);
- $table->setStyle('borderless');
- $table->setHeaders(['IDEs']);
- foreach ($accountIdes as $ide) {
- $appUrlParts = explode('/', $ide->links->application->href);
- $appUuid = end($appUrlParts);
- $application = $applicationResource->get($appUuid);
- $applicationUrl = str_replace('/api', '/a', $application->links->self->href);
-
- $table->addRows([
- ["$ide->label"],
- ["UUID: $ide->uuid"],
- ["Application: $application->name>"],
- ["Subscription: {$application->subscription->name}"],
- ["IDE URL: links->ide->href}>{$ide->links->ide->href}>"],
- ["Web URL: links->web->href}>{$ide->links->web->href}>"],
- new TableSeparator(),
- ]);
- }
- $table->render();
+final class IdeListMineCommand extends IdeCommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $ides = new Ides($acquiaCloudClient);
+ $accountIdes = $ides->getMine();
+ $applicationResource = new Applications($acquiaCloudClient);
+
+ if (count($accountIdes)) {
+ $table = new Table($output);
+ $table->setStyle('borderless');
+ $table->setHeaders(['IDEs']);
+ foreach ($accountIdes as $ide) {
+ $appUrlParts = explode('/', $ide->links->application->href);
+ $appUuid = end($appUrlParts);
+ $application = $applicationResource->get($appUuid);
+ $applicationUrl = str_replace('/api', '/a', $application->links->self->href);
+
+ $table->addRows([
+ ["$ide->label"],
+ ["UUID: $ide->uuid"],
+ ["Application: $application->name>"],
+ ["Subscription: {$application->subscription->name}"],
+ ["IDE URL: links->ide->href}>{$ide->links->ide->href}>"],
+ ["Web URL: links->web->href}>{$ide->links->web->href}>"],
+ new TableSeparator(),
+ ]);
+ }
+ $table->render();
+ } else {
+ $output->writeln('No IDE exists for your account.');
+ }
+
+ return Command::SUCCESS;
}
- else {
- $output->writeln('No IDE exists for your account.');
- }
-
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Ide/IdeOpenCommand.php b/src/Command/Ide/IdeOpenCommand.php
index 0d56f7eca..37f375f30 100644
--- a/src/Command/Ide/IdeOpenCommand.php
+++ b/src/Command/Ide/IdeOpenCommand.php
@@ -1,6 +1,6 @@
setHidden(AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- $this->acceptApplicationUuid();
- // @todo Add option to accept an ide UUID.
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $cloudApplicationUuid = $this->determineCloudApplication();
- $idesResource = new Ides($acquiaCloudClient);
- $ide = $this->promptIdeChoice("Select the IDE you'd like to open:", $idesResource, $cloudApplicationUuid);
-
- $this->output->writeln('');
- $this->output->writeln("Your IDE URL: links->ide->href}>{$ide->links->ide->href}>");
- $this->output->writeln("Your Drupal Site URL: links->web->href}>{$ide->links->web->href}>");
- $this->output->writeln('Opening your IDE in browser...');
-
- $this->localMachineHelper->startBrowser($ide->links->ide->href);
-
- return Command::SUCCESS;
- }
-
+final class IdeOpenCommand extends IdeCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->setHidden(AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
+ $this->acceptApplicationUuid();
+ // @todo Add option to accept an ide UUID.
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $cloudApplicationUuid = $this->determineCloudApplication();
+ $idesResource = new Ides($acquiaCloudClient);
+ $ide = $this->promptIdeChoice("Select the IDE you'd like to open:", $idesResource, $cloudApplicationUuid);
+
+ $this->output->writeln('');
+ $this->output->writeln("Your IDE URL: links->ide->href}>{$ide->links->ide->href}>");
+ $this->output->writeln("Your Drupal Site URL: links->web->href}>{$ide->links->web->href}>");
+ $this->output->writeln('Opening your IDE in browser...');
+
+ $this->localMachineHelper->startBrowser($ide->links->ide->href);
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ide/IdePhpVersionCommand.php b/src/Command/Ide/IdePhpVersionCommand.php
index c647eb000..e0194e004 100644
--- a/src/Command/Ide/IdePhpVersionCommand.php
+++ b/src/Command/Ide/IdePhpVersionCommand.php
@@ -1,6 +1,6 @@
addArgument('version', InputArgument::REQUIRED, 'The PHP version')
- ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
+final class IdePhpVersionCommand extends IdeCommandBase
+{
+ private string $idePhpFilePathPrefix;
+
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('version', InputArgument::REQUIRED, 'The PHP version')
+ ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
+ }
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
- $version = $input->getArgument('version');
- $this->validatePhpVersion($version);
- $this->localMachineHelper->getFilesystem()->dumpFile($this->getIdePhpVersionFilePath(), $version);
- $this->restartService('php-fpm');
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
+ $version = $input->getArgument('version');
+ $this->validatePhpVersion($version);
+ $this->localMachineHelper->getFilesystem()->dumpFile($this->getIdePhpVersionFilePath(), $version);
+ $this->restartService('php-fpm');
- return Command::SUCCESS;
- }
+ return Command::SUCCESS;
+ }
- private function getIdePhpFilePathPrefix(): string {
- if (!isset($this->idePhpFilePathPrefix)) {
- $this->idePhpFilePathPrefix = '/usr/local/php';
+ private function getIdePhpFilePathPrefix(): string
+ {
+ if (!isset($this->idePhpFilePathPrefix)) {
+ $this->idePhpFilePathPrefix = '/usr/local/php';
+ }
+ return $this->idePhpFilePathPrefix;
}
- return $this->idePhpFilePathPrefix;
- }
-
- public function setIdePhpFilePathPrefix(string $path): void {
- $this->idePhpFilePathPrefix = $path;
- }
-
- protected function validatePhpVersion(string $version): string {
- parent::validatePhpVersion($version);
- $phpFilepath = $this->getIdePhpFilePathPrefix() . $version;
- if (!$this->localMachineHelper->getFilesystem()->exists($phpFilepath)) {
- throw new AcquiaCliException('The specified PHP version does not exist on this machine.');
+
+ public function setIdePhpFilePathPrefix(string $path): void
+ {
+ $this->idePhpFilePathPrefix = $path;
}
- return $version;
- }
+ protected function validatePhpVersion(string $version): string
+ {
+ parent::validatePhpVersion($version);
+ $phpFilepath = $this->getIdePhpFilePathPrefix() . $version;
+ if (!$this->localMachineHelper->getFilesystem()->exists($phpFilepath)) {
+ throw new AcquiaCliException('The specified PHP version does not exist on this machine.');
+ }
+ return $version;
+ }
}
diff --git a/src/Command/Ide/IdeServiceRestartCommand.php b/src/Command/Ide/IdeServiceRestartCommand.php
index ca039d8d9..c6a0eee34 100644
--- a/src/Command/Ide/IdeServiceRestartCommand.php
+++ b/src/Command/Ide/IdeServiceRestartCommand.php
@@ -1,6 +1,6 @@
addArgument('service', InputArgument::REQUIRED, 'The name of the service to restart')
- ->addUsage('php')
- ->addUsage('apache')
- ->addUsage('mysql')
- ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
- $service = $input->getArgument('service');
- $this->validateService($service);
-
- $serviceNameMap = [
- 'apache' => 'apache2',
- 'apache2' => 'apache2',
- 'mysql' => 'mysqld',
- 'mysqld' => 'mysqld',
- 'php' => 'php-fpm',
- 'php-fpm' => 'php-fpm',
- ];
- $output->writeln("Restarting $service>...");
- $serviceName = $serviceNameMap[$service];
- $this->restartService($serviceName);
- $output->writeln("Restarted $service>");
-
- return Command::SUCCESS;
- }
-
- private function validateService(string $service): void {
- $violations = Validation::createValidator()->validate($service, [
- new Choice([
- 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'],
- 'message' => 'Specify a valid service name: php, apache, or mysql',
- ]),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+final class IdeServiceRestartCommand extends IdeCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('service', InputArgument::REQUIRED, 'The name of the service to restart')
+ ->addUsage('php')
+ ->addUsage('apache')
+ ->addUsage('mysql')
+ ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
}
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
+ $service = $input->getArgument('service');
+ $this->validateService($service);
+
+ $serviceNameMap = [
+ 'apache' => 'apache2',
+ 'apache2' => 'apache2',
+ 'mysql' => 'mysqld',
+ 'mysqld' => 'mysqld',
+ 'php' => 'php-fpm',
+ 'php-fpm' => 'php-fpm',
+ ];
+ $output->writeln("Restarting $service>...");
+ $serviceName = $serviceNameMap[$service];
+ $this->restartService($serviceName);
+ $output->writeln("Restarted $service>");
+
+ return Command::SUCCESS;
+ }
+ private function validateService(string $service): void
+ {
+ $violations = Validation::createValidator()->validate($service, [
+ new Choice([
+ 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'],
+ 'message' => 'Specify a valid service name: php, apache, or mysql',
+ ]),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ }
}
diff --git a/src/Command/Ide/IdeServiceStartCommand.php b/src/Command/Ide/IdeServiceStartCommand.php
index 5d3f0d607..71a5e19b0 100644
--- a/src/Command/Ide/IdeServiceStartCommand.php
+++ b/src/Command/Ide/IdeServiceStartCommand.php
@@ -1,6 +1,6 @@
addArgument('service', InputArgument::REQUIRED, 'The name of the service to start')
- ->addUsage('php')
- ->addUsage('apache')
- ->addUsage('mysql')
- ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
- $service = $input->getArgument('service');
- $this->validateService($service);
-
- $serviceNameMap = [
- 'apache' => 'apache2',
- 'apache2' => 'apache2',
- 'mysql' => 'mysqld',
- 'mysqld' => 'mysqld',
- 'php' => 'php-fpm',
- 'php-fpm' => 'php-fpm',
- ];
- $output->writeln("Starting $service>...");
- $serviceName = $serviceNameMap[$service];
- $this->startService($serviceName);
- $output->writeln("Started $service>");
-
- return Command::SUCCESS;
- }
-
- private function validateService(string $service): void {
- $violations = Validation::createValidator()->validate($service, [
- new Choice([
- 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'],
- 'message' => 'Specify a valid service name: php, apache, or mysql',
- ]),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+final class IdeServiceStartCommand extends IdeCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('service', InputArgument::REQUIRED, 'The name of the service to start')
+ ->addUsage('php')
+ ->addUsage('apache')
+ ->addUsage('mysql')
+ ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
}
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
+ $service = $input->getArgument('service');
+ $this->validateService($service);
+
+ $serviceNameMap = [
+ 'apache' => 'apache2',
+ 'apache2' => 'apache2',
+ 'mysql' => 'mysqld',
+ 'mysqld' => 'mysqld',
+ 'php' => 'php-fpm',
+ 'php-fpm' => 'php-fpm',
+ ];
+ $output->writeln("Starting $service>...");
+ $serviceName = $serviceNameMap[$service];
+ $this->startService($serviceName);
+ $output->writeln("Started $service>");
+
+ return Command::SUCCESS;
+ }
+ private function validateService(string $service): void
+ {
+ $violations = Validation::createValidator()->validate($service, [
+ new Choice([
+ 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'],
+ 'message' => 'Specify a valid service name: php, apache, or mysql',
+ ]),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ }
}
diff --git a/src/Command/Ide/IdeServiceStopCommand.php b/src/Command/Ide/IdeServiceStopCommand.php
index d0e9476d8..039015a1d 100644
--- a/src/Command/Ide/IdeServiceStopCommand.php
+++ b/src/Command/Ide/IdeServiceStopCommand.php
@@ -1,6 +1,6 @@
addArgument('service', InputArgument::REQUIRED, 'The name of the service to stop')
- ->addUsage('php')
- ->addUsage('apache')
- ->addUsage('mysql')
- ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
- $service = $input->getArgument('service');
- $this->validateService($service);
-
- $serviceNameMap = [
- 'apache' => 'apache2',
- 'apache2' => 'apache2',
- 'mysql' => 'mysqld',
- 'mysqld' => 'mysqld',
- 'php' => 'php-fpm',
- 'php-fpm' => 'php-fpm',
- ];
- $output->writeln("Stopping $service>...");
- $serviceName = $serviceNameMap[$service];
- $this->stopService($serviceName);
- $output->writeln("Stopped $service>");
-
- return Command::SUCCESS;
- }
-
- private function validateService(string $service): void {
- $violations = Validation::createValidator()->validate($service, [
- new Choice([
- 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'],
- 'message' => 'Specify a valid service name: php, apache, or mysql',
- ]),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+final class IdeServiceStopCommand extends IdeCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('service', InputArgument::REQUIRED, 'The name of the service to stop')
+ ->addUsage('php')
+ ->addUsage('apache')
+ ->addUsage('mysql')
+ ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
}
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
+ $service = $input->getArgument('service');
+ $this->validateService($service);
+
+ $serviceNameMap = [
+ 'apache' => 'apache2',
+ 'apache2' => 'apache2',
+ 'mysql' => 'mysqld',
+ 'mysqld' => 'mysqld',
+ 'php' => 'php-fpm',
+ 'php-fpm' => 'php-fpm',
+ ];
+ $output->writeln("Stopping $service>...");
+ $serviceName = $serviceNameMap[$service];
+ $this->stopService($serviceName);
+ $output->writeln("Stopped $service>");
+
+ return Command::SUCCESS;
+ }
+ private function validateService(string $service): void
+ {
+ $violations = Validation::createValidator()->validate($service, [
+ new Choice([
+ 'choices' => ['php', 'php-fpm', 'apache', 'apache2', 'mysql', 'mysqld'],
+ 'message' => 'Specify a valid service name: php, apache, or mysql',
+ ]),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+ }
}
diff --git a/src/Command/Ide/IdeShareCommand.php b/src/Command/Ide/IdeShareCommand.php
index 46fdf0d5a..f01240c89 100644
--- a/src/Command/Ide/IdeShareCommand.php
+++ b/src/Command/Ide/IdeShareCommand.php
@@ -1,6 +1,6 @@
+ */
+ private array $shareCodeFilepaths;
- /**
- * @var array
- */
- private array $shareCodeFilepaths;
-
- protected function configure(): void {
- $this
- ->addOption('regenerate', '', InputOption::VALUE_NONE, 'regenerate the share code')
- ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
-
- if ($input->getOption('regenerate')) {
- $this->regenerateShareCode();
+ protected function configure(): void
+ {
+ $this
+ ->addOption('regenerate', '', InputOption::VALUE_NONE, 'regenerate the share code')
+ ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
}
- $shareUuid = $this->localMachineHelper->readFile($this->getShareCodeFilepaths()[0]);
- $webUrl = self::getThisCloudIdeWebUrl();
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
- $this->output->writeln('');
- $this->output->writeln("Your IDE Share URL: https://$webUrl?share=$shareUuid>");
+ if ($input->getOption('regenerate')) {
+ $this->regenerateShareCode();
+ }
- return Command::SUCCESS;
- }
+ $shareUuid = $this->localMachineHelper->readFile($this->getShareCodeFilepaths()[0]);
+ $webUrl = self::getThisCloudIdeWebUrl();
- public function setShareCodeFilepaths(array $filePath): void {
- $this->shareCodeFilepaths = $filePath;
- }
+ $this->output->writeln('');
+ $this->output->writeln("Your IDE Share URL: https://$webUrl?share=$shareUuid>");
- /**
- * @return array
- */
- private function getShareCodeFilepaths(): array {
- if (!isset($this->shareCodeFilepaths)) {
- $this->shareCodeFilepaths = [
- '/usr/local/share/ide/.sharecode',
- '/home/ide/.sharecode',
- ];
+ return Command::SUCCESS;
}
- return $this->shareCodeFilepaths;
- }
- private function regenerateShareCode(): void {
- $newShareCode = (string) Uuid::uuid4();
- foreach ($this->getShareCodeFilepaths() as $path) {
- $this->localMachineHelper->writeFile($path, $newShareCode);
+ public function setShareCodeFilepaths(array $filePath): void
+ {
+ $this->shareCodeFilepaths = $filePath;
}
- }
+ /**
+ * @return array
+ */
+ private function getShareCodeFilepaths(): array
+ {
+ if (!isset($this->shareCodeFilepaths)) {
+ $this->shareCodeFilepaths = [
+ '/usr/local/share/ide/.sharecode',
+ '/home/ide/.sharecode',
+ ];
+ }
+ return $this->shareCodeFilepaths;
+ }
+
+ private function regenerateShareCode(): void
+ {
+ $newShareCode = (string) Uuid::uuid4();
+ foreach ($this->getShareCodeFilepaths() as $path) {
+ $this->localMachineHelper->writeFile($path, $newShareCode);
+ }
+ }
}
diff --git a/src/Command/Ide/IdeXdebugToggleCommand.php b/src/Command/Ide/IdeXdebugToggleCommand.php
index a003ca2ea..14d28f7b8 100644
--- a/src/Command/Ide/IdeXdebugToggleCommand.php
+++ b/src/Command/Ide/IdeXdebugToggleCommand.php
@@ -1,6 +1,6 @@
setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
+ }
- protected function configure(): void {
- $this
- ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
+ $iniFile = $this->getXdebugIniFilePath();
+ $contents = file_get_contents($iniFile);
+ $this->setXDebugStatus($contents);
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
- $iniFile = $this->getXdebugIniFilePath();
- $contents = file_get_contents($iniFile);
- $this->setXDebugStatus($contents);
+ if ($this->getXDebugStatus() === false) {
+ $this->enableXDebug($iniFile, $contents);
+ } elseif ($this->getXDebugStatus() === true) {
+ $this->disableXDebug($iniFile, $contents);
+ } else {
+ throw new AcquiaCliException("Could not find xdebug zend extension in $iniFile!");
+ }
+ $this->restartService('php-fpm');
- if ($this->getXDebugStatus() === FALSE) {
- $this->enableXDebug($iniFile, $contents);
- }
- elseif ($this->getXDebugStatus() === TRUE) {
- $this->disableXDebug($iniFile, $contents);
- }
- else {
- throw new AcquiaCliException("Could not find xdebug zend extension in $iniFile!");
+ return Command::SUCCESS;
}
- $this->restartService('php-fpm');
- return Command::SUCCESS;
- }
-
- /**
- * Sets $this->xDebugEnabled.
- *
- * @param string $contents The contents of php.ini.
- */
- private function setXDebugStatus(string $contents): void {
- if (str_contains($contents, ';zend_extension=xdebug.so')) {
- $this->xDebugEnabled = FALSE;
- }
- elseif (str_contains($contents, 'zend_extension=xdebug.so')) {
- $this->xDebugEnabled = TRUE;
+ /**
+ * Sets $this->xDebugEnabled.
+ *
+ * @param string $contents The contents of php.ini.
+ */
+ private function setXDebugStatus(string $contents): void
+ {
+ if (str_contains($contents, ';zend_extension=xdebug.so')) {
+ $this->xDebugEnabled = false;
+ } elseif (str_contains($contents, 'zend_extension=xdebug.so')) {
+ $this->xDebugEnabled = true;
+ } else {
+ $this->xDebugEnabled = null;
+ }
}
- else {
- $this->xDebugEnabled = NULL;
- }
- }
-
- private function getXDebugStatus(): ?bool {
- return $this->xDebugEnabled;
- }
- /**
- * Enables xDebug.
- *
- * @param string $contents The contents of php.ini.
- */
- private function enableXDebug(string $destinationFile, string $contents): void {
- $this->logger->notice("Enabling Xdebug PHP extension in $destinationFile...");
+ private function getXDebugStatus(): ?bool
+ {
+ return $this->xDebugEnabled;
+ }
- // Note that this replaces 1 or more ";" characters.
- $newContents = preg_replace('/(;)+(zend_extension=xdebug\.so)/', '$2', $contents);
- file_put_contents($destinationFile, $newContents);
- $this->output->writeln("Xdebug PHP extension enabled.");
- $this->output->writeln("You must also enable Xdebug listening in your code editor to begin a debugging session.");
- }
+ /**
+ * Enables xDebug.
+ *
+ * @param string $contents The contents of php.ini.
+ */
+ private function enableXDebug(string $destinationFile, string $contents): void
+ {
+ $this->logger->notice("Enabling Xdebug PHP extension in $destinationFile...");
- /**
- * Disables xDebug.
- *
- * @param string $contents The contents of php.ini.
- */
- private function disableXDebug(string $destinationFile, string $contents): void {
- $this->logger->notice("Disabling Xdebug PHP extension in $destinationFile...");
- $newContents = preg_replace('/(;)*(zend_extension=xdebug\.so)/', ';$2', $contents);
- file_put_contents($destinationFile, $newContents);
- $this->output->writeln("Xdebug PHP extension disabled.");
- }
+ // Note that this replaces 1 or more ";" characters.
+ $newContents = preg_replace('/(;)+(zend_extension=xdebug\.so)/', '$2', $contents);
+ file_put_contents($destinationFile, $newContents);
+ $this->output->writeln("Xdebug PHP extension enabled.");
+ $this->output->writeln("You must also enable Xdebug listening in your code editor to begin a debugging session.");
+ }
+ /**
+ * Disables xDebug.
+ *
+ * @param string $contents The contents of php.ini.
+ */
+ private function disableXDebug(string $destinationFile, string $contents): void
+ {
+ $this->logger->notice("Disabling Xdebug PHP extension in $destinationFile...");
+ $newContents = preg_replace('/(;)*(zend_extension=xdebug\.so)/', ';$2', $contents);
+ file_put_contents($destinationFile, $newContents);
+ $this->output->writeln("Xdebug PHP extension disabled.");
+ }
}
diff --git a/src/Command/Ide/Wizard/IdeWizardCommandBase.php b/src/Command/Ide/Wizard/IdeWizardCommandBase.php
index 43cccbb19..34f43310f 100644
--- a/src/Command/Ide/Wizard/IdeWizardCommandBase.php
+++ b/src/Command/Ide/Wizard/IdeWizardCommandBase.php
@@ -1,6 +1,6 @@
setSshKeyFilepath(self::getSshKeyFilename($this::getThisCloudIdeUuid()));
- $this->passphraseFilepath = $this->localMachineHelper->getLocalFilepath('~/.passphrase');
- }
-
- public static function getSshKeyFilename(mixed $ideUuid): string {
- return 'id_rsa_acquia_ide_' . $ideUuid;
- }
+ $this->setSshKeyFilepath(self::getSshKeyFilename($this::getThisCloudIdeUuid()));
+ $this->passphraseFilepath = $this->localMachineHelper->getLocalFilepath('~/.passphrase');
+ }
- protected function validateEnvironment(): void {
- $this->requireCloudIdeEnvironment();
- }
+ public static function getSshKeyFilename(mixed $ideUuid): string
+ {
+ return 'id_rsa_acquia_ide_' . $ideUuid;
+ }
- protected function getSshKeyLabel(): string {
- return $this::getIdeSshKeyLabel(self::getThisCloudIdeLabel(), self::getThisCloudIdeUuid());
- }
+ protected function validateEnvironment(): void
+ {
+ $this->requireCloudIdeEnvironment();
+ }
- protected function deleteThisSshKeyFromCloud(mixed $output): void {
- if ($cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid())) {
- $this->deleteSshKeyFromCloud($output, $cloudKey);
+ protected function getSshKeyLabel(): string
+ {
+ return $this::getIdeSshKeyLabel(self::getThisCloudIdeLabel(), self::getThisCloudIdeUuid());
}
- }
+ protected function deleteThisSshKeyFromCloud(mixed $output): void
+ {
+ if ($cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid())) {
+ $this->deleteSshKeyFromCloud($output, $cloudKey);
+ }
+ }
}
diff --git a/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php b/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php
index d25b7beae..f8383b732 100644
--- a/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php
+++ b/src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php
@@ -1,6 +1,6 @@
setHidden(!CommandBase::isAcquiaCloudIde());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $checklist = new Checklist($output);
-
- // Get Cloud account.
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $accountAdapter = new Account($acquiaCloudClient);
- $account = $accountAdapter->get();
- $this->validateRequiredCloudPermissions(
- $acquiaCloudClient,
- self::getThisCloudIdeCloudAppUuid(),
- $account,
- [
- // Add SSH key to git repository.
- "add ssh key to git",
- // Add SSH key to non-production environments.
- "add ssh key to non-prod",
- ]
- );
-
- $keyWasUploaded = FALSE;
- // Create local SSH key.
- if (!$this->localSshKeyExists() || !$this->passPhraseFileExists()) {
- // Just in case the public key exists and the private doesn't, remove the public key.
- $this->deleteLocalSshKey();
- // Just in case there's an orphaned key on the Cloud Platform for this Cloud IDE.
- $this->deleteThisSshKeyFromCloud($output);
-
- $checklist->addItem('Creating a local SSH key');
-
- // Create SSH key.
- $password = md5(random_bytes(10));
- $this->savePassPhraseToFile($password);
- $this->createSshKey($this->privateSshKeyFilename, $password);
-
- $checklist->completePreviousItem();
- $keyWasUploaded = TRUE;
- }
- else {
- $checklist->addItem('Already created a local key');
- $checklist->completePreviousItem();
- }
-
- // Upload SSH key to the Cloud Platform.
- if (!$this->userHasUploadedThisKeyToCloud($this->getSshKeyLabel())) {
- $checklist->addItem('Uploading the local key to the Cloud Platform');
-
- // Just in case there is an uploaded key, but it doesn't actually match
- // the local key, delete remote key!
- $this->deleteThisSshKeyFromCloud($output);
- $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath);
- $this->uploadSshKey($this->getSshKeyLabel(), $publicKey);
-
- $checklist->completePreviousItem();
- $keyWasUploaded = TRUE;
- }
- else {
- $checklist->addItem('Already uploaded the local key to the Cloud Platform');
- $checklist->completePreviousItem();
+final class IdeWizardCreateSshKeyCommand extends IdeWizardCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->setHidden(!CommandBase::isAcquiaCloudIde());
}
- // Add SSH key to local keychain.
- if (!$this->sshKeyIsAddedToKeychain()) {
- $checklist->addItem('Adding the SSH key to local keychain');
- $this->addSshKeyToAgent($this->publicSshKeyFilepath, $this->getPassPhraseFromFile());
- }
- else {
- $checklist->addItem('Already added the SSH key to local keychain');
- }
- $checklist->completePreviousItem();
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $checklist = new Checklist($output);
+
+ // Get Cloud account.
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $accountAdapter = new Account($acquiaCloudClient);
+ $account = $accountAdapter->get();
+ $this->validateRequiredCloudPermissions(
+ $acquiaCloudClient,
+ self::getThisCloudIdeCloudAppUuid(),
+ $account,
+ [
+ // Add SSH key to git repository.
+ "add ssh key to git",
+ // Add SSH key to non-production environments.
+ "add ssh key to non-prod",
+ ]
+ );
+
+ $keyWasUploaded = false;
+ // Create local SSH key.
+ if (!$this->localSshKeyExists() || !$this->passPhraseFileExists()) {
+ // Just in case the public key exists and the private doesn't, remove the public key.
+ $this->deleteLocalSshKey();
+ // Just in case there's an orphaned key on the Cloud Platform for this Cloud IDE.
+ $this->deleteThisSshKeyFromCloud($output);
+
+ $checklist->addItem('Creating a local SSH key');
+
+ // Create SSH key.
+ $password = md5(random_bytes(10));
+ $this->savePassPhraseToFile($password);
+ $this->createSshKey($this->privateSshKeyFilename, $password);
+
+ $checklist->completePreviousItem();
+ $keyWasUploaded = true;
+ } else {
+ $checklist->addItem('Already created a local key');
+ $checklist->completePreviousItem();
+ }
+
+ // Upload SSH key to the Cloud Platform.
+ if (!$this->userHasUploadedThisKeyToCloud($this->getSshKeyLabel())) {
+ $checklist->addItem('Uploading the local key to the Cloud Platform');
+
+ // Just in case there is an uploaded key, but it doesn't actually match
+ // the local key, delete remote key!
+ $this->deleteThisSshKeyFromCloud($output);
+ $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath);
+ $this->uploadSshKey($this->getSshKeyLabel(), $publicKey);
+
+ $checklist->completePreviousItem();
+ $keyWasUploaded = true;
+ } else {
+ $checklist->addItem('Already uploaded the local key to the Cloud Platform');
+ $checklist->completePreviousItem();
+ }
+
+ // Add SSH key to local keychain.
+ if (!$this->sshKeyIsAddedToKeychain()) {
+ $checklist->addItem('Adding the SSH key to local keychain');
+ $this->addSshKeyToAgent($this->publicSshKeyFilepath, $this->getPassPhraseFromFile());
+ } else {
+ $checklist->addItem('Already added the SSH key to local keychain');
+ }
+ $checklist->completePreviousItem();
+
+ // Wait for the key to register on the Cloud Platform.
+ if ($keyWasUploaded) {
+ if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) {
+ $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.');
+ return Command::SUCCESS;
+ }
+ $this->pollAcquiaCloudUntilSshSuccess($output);
+ }
- // Wait for the key to register on the Cloud Platform.
- if ($keyWasUploaded) {
- if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) {
- $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.');
return Command::SUCCESS;
- }
- $this->pollAcquiaCloudUntilSshSuccess($output);
}
-
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php b/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php
index d1a07faa9..6826f4f69 100644
--- a/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php
+++ b/src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php
@@ -1,6 +1,6 @@
setHidden(!CommandBase::isAcquiaCloudIde());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->requireCloudIdeEnvironment();
-
- $cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid());
- if (!$cloudKey) {
- throw new AcquiaCliException('Could not find an SSH key on the Cloud Platform matching any local key in this IDE.');
+final class IdeWizardDeleteSshKeyCommand extends IdeWizardCommandBase
+{
+ use SshCommandTrait;
+
+ protected function configure(): void
+ {
+ $this
+ ->setHidden(!CommandBase::isAcquiaCloudIde());
}
- $this->deleteSshKeyFromCloud($output, $cloudKey);
- $this->deleteLocalSshKey();
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->requireCloudIdeEnvironment();
+
+ $cloudKey = $this->findIdeSshKeyOnCloud($this::getThisCloudIdeLabel(), $this::getThisCloudIdeUuid());
+ if (!$cloudKey) {
+ throw new AcquiaCliException('Could not find an SSH key on the Cloud Platform matching any local key in this IDE.');
+ }
- $this->output->writeln("Deleted local files {$this->publicSshKeyFilepath}> and {$this->privateSshKeyFilepath}>");
+ $this->deleteSshKeyFromCloud($output, $cloudKey);
+ $this->deleteLocalSshKey();
- return Command::SUCCESS;
- }
+ $this->output->writeln("Deleted local files {$this->publicSshKeyFilepath}> and {$this->privateSshKeyFilepath}>");
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Pull/PullCodeCommand.php b/src/Command/Pull/PullCodeCommand.php
index b4a57409a..add6093b1 100644
--- a/src/Command/Pull/PullCodeCommand.php
+++ b/src/Command/Pull/PullCodeCommand.php
@@ -1,6 +1,6 @@
acceptEnvironmentId()
- ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed')
- ->addOption('no-scripts', NULL, InputOption::VALUE_NONE,
- 'Do not run any additional scripts after code is pulled. E.g., composer install , drush cache-rebuild, etc.');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->setDirAndRequireProjectCwd($input);
- $clone = $this->determineCloneProject($output);
- $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE);
- $this->pullCode($input, $output, $clone, $sourceEnvironment);
- $this->checkEnvironmentPhpVersions($sourceEnvironment);
- $this->matchIdePhpVersion($output, $sourceEnvironment);
- if (!$input->getOption('no-scripts')) {
- $outputCallback = $this->getOutputCallback($output, $this->checklist);
- $this->runComposerScripts($outputCallback, $this->checklist);
- $this->runDrushCacheClear($outputCallback, $this->checklist);
+final class PullCodeCommand extends PullCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId()
+ ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed')
+ ->addOption(
+ 'no-scripts',
+ null,
+ InputOption::VALUE_NONE,
+ 'Do not run any additional scripts after code is pulled. E.g., composer install , drush cache-rebuild, etc.'
+ );
}
- return Command::SUCCESS;
- }
-
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ $clone = $this->determineCloneProject($output);
+ $sourceEnvironment = $this->determineEnvironment($input, $output, true);
+ $this->pullCode($input, $output, $clone, $sourceEnvironment);
+ $this->checkEnvironmentPhpVersions($sourceEnvironment);
+ $this->matchIdePhpVersion($output, $sourceEnvironment);
+ if (!$input->getOption('no-scripts')) {
+ $outputCallback = $this->getOutputCallback($output, $this->checklist);
+ $this->runComposerScripts($outputCallback, $this->checklist);
+ $this->runDrushCacheClear($outputCallback, $this->checklist);
+ }
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Pull/PullCommand.php b/src/Command/Pull/PullCommand.php
index 7c632dac8..b01d0440a 100644
--- a/src/Command/Pull/PullCommand.php
+++ b/src/Command/Pull/PullCommand.php
@@ -1,6 +1,6 @@
acceptEnvironmentId()
- ->acceptSite()
- ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed')
- ->addOption('no-code', NULL, InputOption::VALUE_NONE, 'Do not refresh code from remote repository')
- ->addOption('no-files', NULL, InputOption::VALUE_NONE, 'Do not refresh files')
- ->addOption('no-databases', NULL, InputOption::VALUE_NONE, 'Do not refresh databases')
- ->addOption(
+final class PullCommand extends PullCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId()
+ ->acceptSite()
+ ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed')
+ ->addOption('no-code', null, InputOption::VALUE_NONE, 'Do not refresh code from remote repository')
+ ->addOption('no-files', null, InputOption::VALUE_NONE, 'Do not refresh files')
+ ->addOption('no-databases', null, InputOption::VALUE_NONE, 'Do not refresh databases')
+ ->addOption(
'no-scripts',
- NULL,
+ null,
InputOption::VALUE_NONE,
'Do not run any additional scripts after code and database are copied. E.g., composer install , drush cache-rebuild, etc.'
);
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->setDirAndRequireProjectCwd($input);
- $clone = $this->determineCloneProject($output);
- $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE);
-
- if (!$input->getOption('no-code')) {
- $this->pullCode($input, $output, $clone, $sourceEnvironment);
}
- if (!$input->getOption('no-files')) {
- $this->pullFiles($input, $output, $sourceEnvironment);
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ $clone = $this->determineCloneProject($output);
+ $sourceEnvironment = $this->determineEnvironment($input, $output, true);
- if (!$input->getOption('no-databases')) {
- $this->pullDatabase($input, $output, $sourceEnvironment);
- }
+ if (!$input->getOption('no-code')) {
+ $this->pullCode($input, $output, $clone, $sourceEnvironment);
+ }
- if (!$input->getOption('no-scripts')) {
- $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist);
- }
+ if (!$input->getOption('no-files')) {
+ $this->pullFiles($input, $output, $sourceEnvironment);
+ }
+
+ if (!$input->getOption('no-databases')) {
+ $this->pullDatabase($input, $output, $sourceEnvironment);
+ }
- return Command::SUCCESS;
- }
+ if (!$input->getOption('no-scripts')) {
+ $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist);
+ }
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php
index ce18ba740..84b93117a 100644
--- a/src/Command/Pull/PullCommandBase.php
+++ b/src/Command/Pull/PullCommandBase.php
@@ -1,6 +1,6 @@
localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger);
- }
-
- /**
- * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L168
- * @return string[]
- */
- private function listTables(string $out): array {
- $tables = [];
- if ($out = trim($out)) {
- $tables = explode(PHP_EOL, $out);
- }
- return $tables;
- }
-
- /**
- * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L178
- * @return string[]
- */
- private function listTablesQuoted(string $out): array {
- $tables = $this->listTables($out);
- foreach ($tables as &$table) {
- $table = "`$table`";
- }
- return $tables;
- }
-
- public static function getBackupPath(object $environment, DatabaseResponse $database, object $backupResponse): string {
- // Databases have a machine name not exposed via the API; we can only
- // approximately reconstruct it and match the filename you'd get downloading
- // a backup from Cloud UI.
- if ($database->flags->default) {
- $dbMachineName = $database->name . $environment->name;
- }
- else {
- $dbMachineName = 'db' . $database->id;
- }
- $filename = implode('-', [
+abstract class PullCommandBase extends CommandBase
+{
+ use IdeCommandTrait;
+
+ protected Checklist $checklist;
+
+ private string $site;
+
+ private UriInterface $backupDownloadUrl;
+
+ public function __construct(
+ public LocalMachineHelper $localMachineHelper,
+ protected CloudDataStore $datastoreCloud,
+ protected AcquiaCliDatastore $datastoreAcli,
+ protected ApiCredentialsInterface $cloudCredentials,
+ protected TelemetryHelper $telemetryHelper,
+ protected string $projectDir,
+ protected ClientService $cloudApiClientService,
+ public SshHelper $sshHelper,
+ protected string $sshDir,
+ LoggerInterface $logger,
+ protected \GuzzleHttp\Client $httpClient
+ ) {
+ parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger);
+ }
+
+ /**
+ * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L168
+ * @return string[]
+ */
+ private function listTables(string $out): array
+ {
+ $tables = [];
+ if ($out = trim($out)) {
+ $tables = explode(PHP_EOL, $out);
+ }
+ return $tables;
+ }
+
+ /**
+ * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L178
+ * @return string[]
+ */
+ private function listTablesQuoted(string $out): array
+ {
+ $tables = $this->listTables($out);
+ foreach ($tables as &$table) {
+ $table = "`$table`";
+ }
+ return $tables;
+ }
+
+ public static function getBackupPath(object $environment, DatabaseResponse $database, object $backupResponse): string
+ {
+ // Databases have a machine name not exposed via the API; we can only
+ // approximately reconstruct it and match the filename you'd get downloading
+ // a backup from Cloud UI.
+ if ($database->flags->default) {
+ $dbMachineName = $database->name . $environment->name;
+ } else {
+ $dbMachineName = 'db' . $database->id;
+ }
+ $filename = implode('-', [
$environment->name,
$database->name,
$dbMachineName,
$backupResponse->completedAt,
- ]) . '.sql.gz';
- return Path::join(sys_get_temp_dir(), $filename);
- }
-
- protected function initialize(InputInterface $input, OutputInterface $output): void {
- parent::initialize($input, $output);
- $this->checklist = new Checklist($output);
- }
-
- protected function pullCode(InputInterface $input, OutputInterface $output, bool $clone, EnvironmentResponse $sourceEnvironment): void {
- if ($clone) {
- $this->checklist->addItem('Cloning git repository from the Cloud Platform');
- $this->cloneFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist));
- }
- else {
- $this->checklist->addItem('Pulling code from the Cloud Platform');
- $this->pullCodeFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist));
+ ]) . '.sql.gz';
+ return Path::join(sys_get_temp_dir(), $filename);
}
- $this->checklist->completePreviousItem();
- }
-
- /**
- * @param bool $onDemand Force on-demand backup.
- * @param bool $noImport Skip import.
- */
- protected function pullDatabase(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment, bool $onDemand = FALSE, bool $noImport = FALSE, bool $multipleDbs = FALSE): void {
- if (!$noImport) {
- // Verify database connection.
- $this->connectToLocalDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $this->getOutputCallback($output, $this->checklist));
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ parent::initialize($input, $output);
+ $this->checklist = new Checklist($output);
}
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $site = $this->determineSite($sourceEnvironment, $input);
- $databases = $this->determineCloudDatabases($acquiaCloudClient, $sourceEnvironment, $site, $multipleDbs);
-
- foreach ($databases as $database) {
- if ($onDemand) {
- $this->checklist->addItem("Creating an on-demand database(s) backup on Cloud Platform");
- $this->createBackup($sourceEnvironment, $database, $acquiaCloudClient);
- $this->checklist->completePreviousItem();
- }
- $backupResponse = $this->getDatabaseBackup($acquiaCloudClient, $sourceEnvironment, $database);
- if (!$onDemand) {
- $this->printDatabaseBackupInfo($backupResponse, $sourceEnvironment);
- }
-
- $this->checklist->addItem("Downloading $database->name database copy from the Cloud Platform");
- $localFilepath = $this->downloadDatabaseBackup($sourceEnvironment, $database, $backupResponse, $this->getOutputCallback($output, $this->checklist));
- $this->checklist->completePreviousItem();
-
- if ($noImport) {
- $this->io->success("$database->name database backup downloaded to $localFilepath");
- }
- else {
- $this->checklist->addItem("Importing $database->name database download");
- $this->importRemoteDatabase($database, $localFilepath, $this->getOutputCallback($output, $this->checklist));
+
+ protected function pullCode(InputInterface $input, OutputInterface $output, bool $clone, EnvironmentResponse $sourceEnvironment): void
+ {
+ if ($clone) {
+ $this->checklist->addItem('Cloning git repository from the Cloud Platform');
+ $this->cloneFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist));
+ } else {
+ $this->checklist->addItem('Pulling code from the Cloud Platform');
+ $this->pullCodeFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist));
+ }
$this->checklist->completePreviousItem();
- }
- }
- }
-
- protected function pullFiles(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment): void {
- $this->checklist->addItem('Copying Drupal\'s public files from the Cloud Platform');
- $site = $this->determineSite($sourceEnvironment, $input);
- $this->rsyncFilesFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist), $site);
- $this->checklist->completePreviousItem();
- }
-
- private function pullCodeFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback = NULL): void {
- $isDirty = $this->isLocalGitRepoDirty();
- if ($isDirty) {
- throw new AcquiaCliException('Pulling code from your Cloud Platform environment was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.');
- }
- // @todo Validate that an Acquia remote is configured for this repository.
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $this->localMachineHelper->execute([
- 'git',
- 'fetch',
- '--all',
- ], $outputCallback, $this->dir, FALSE);
- $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback);
- }
-
- /**
- * Checks out the matching branch from a source environment.
- */
- private function checkoutBranchFromEnv(EnvironmentResponse $environment, Closure $outputCallback = NULL): void {
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $this->localMachineHelper->execute([
- 'git',
- 'checkout',
- $environment->vcs->path,
- ], $outputCallback, $this->dir, FALSE);
- }
-
- private function doImportRemoteDatabase(
- string $databaseHost,
- string $databaseUser,
- string $databaseName,
- string $databasePassword,
- string $localFilepath,
- Closure $outputCallback = NULL
- ): void {
- $this->dropDbTables($databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback);
- $this->importDatabaseDump($localFilepath, $databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback);
- $this->localMachineHelper->getFilesystem()->remove($localFilepath);
- }
-
- private function downloadDatabaseBackup(
- EnvironmentResponse $environment,
- DatabaseResponse $database,
- BackupResponse $backupResponse,
- callable $outputCallback = NULL
- ): string {
- if ($outputCallback) {
- $outputCallback('out', "Downloading backup $backupResponse->id");
- }
- $localFilepath = self::getBackupPath($environment, $database, $backupResponse);
- if ($this->output instanceof ConsoleOutput) {
- $output = $this->output->section();
}
- else {
- $output = $this->output;
+
+ /**
+ * @param bool $onDemand Force on-demand backup.
+ * @param bool $noImport Skip import.
+ */
+ protected function pullDatabase(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment, bool $onDemand = false, bool $noImport = false, bool $multipleDbs = false): void
+ {
+ if (!$noImport) {
+ // Verify database connection.
+ $this->connectToLocalDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $this->getOutputCallback($output, $this->checklist));
+ }
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $site = $this->determineSite($sourceEnvironment, $input);
+ $databases = $this->determineCloudDatabases($acquiaCloudClient, $sourceEnvironment, $site, $multipleDbs);
+
+ foreach ($databases as $database) {
+ if ($onDemand) {
+ $this->checklist->addItem("Creating an on-demand database(s) backup on Cloud Platform");
+ $this->createBackup($sourceEnvironment, $database, $acquiaCloudClient);
+ $this->checklist->completePreviousItem();
+ }
+ $backupResponse = $this->getDatabaseBackup($acquiaCloudClient, $sourceEnvironment, $database);
+ if (!$onDemand) {
+ $this->printDatabaseBackupInfo($backupResponse, $sourceEnvironment);
+ }
+
+ $this->checklist->addItem("Downloading $database->name database copy from the Cloud Platform");
+ $localFilepath = $this->downloadDatabaseBackup($sourceEnvironment, $database, $backupResponse, $this->getOutputCallback($output, $this->checklist));
+ $this->checklist->completePreviousItem();
+
+ if ($noImport) {
+ $this->io->success("$database->name database backup downloaded to $localFilepath");
+ } else {
+ $this->checklist->addItem("Importing $database->name database download");
+ $this->importRemoteDatabase($database, $localFilepath, $this->getOutputCallback($output, $this->checklist));
+ $this->checklist->completePreviousItem();
+ }
+ }
}
- // These options tell curl to stream the file to disk rather than loading it into memory.
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $acquiaCloudClient->addOption('sink', $localFilepath);
- $acquiaCloudClient->addOption('curl.options', [
- 'CURLOPT_FILE' => $localFilepath,
- 'CURLOPT_RETURNTRANSFER' => FALSE,
-]);
- $acquiaCloudClient->addOption('progress', static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $output): void {
- self::displayDownloadProgress($totalBytes, $downloadedBytes, $progress, $output);
- });
- // This is really just used to allow us to inject values for $url during testing.
- // It should be empty during normal operations.
- $url = $this->getBackupDownloadUrl();
- $acquiaCloudClient->addOption('on_stats', function (TransferStats $stats) use (&$url): void {
- $url = $stats->getEffectiveUri();
- });
-
- try {
- $acquiaCloudClient->stream("get", "/environments/$environment->uuid/databases/$database->name/backups/$backupResponse->id/actions/download", $acquiaCloudClient->getOptions());
- return $localFilepath;
+
+ protected function pullFiles(InputInterface $input, OutputInterface $output, EnvironmentResponse $sourceEnvironment): void
+ {
+ $this->checklist->addItem('Copying Drupal\'s public files from the Cloud Platform');
+ $site = $this->determineSite($sourceEnvironment, $input);
+ $this->rsyncFilesFromCloud($sourceEnvironment, $this->getOutputCallback($output, $this->checklist), $site);
+ $this->checklist->completePreviousItem();
}
- catch (RequestException $exception) {
- // Deal with broken SSL certificates.
- // @see https://timi.eu/docs/anatella/5_1_9_1_list_of_curl_error_codes.html
- if (in_array($exception->getHandlerContext()['errno'], [51, 60], TRUE)) {
- $outputCallback('out', 'The certificate for ' . $url->getHost() . ' is invalid.');
- assert($url !== NULL);
- $domainsResource = new Domains($this->cloudApiClientService->getClient());
- $domains = $domainsResource->getAll($environment->uuid);
- foreach ($domains as $domain) {
- if ($domain->hostname === $url->getHost()) {
- continue;
- }
- $outputCallback('out', 'Trying alternative host ' . $domain->hostname . ' ');
- $downloadUrl = $url->withHost($domain->hostname);
- try {
- $this->httpClient->request('GET', $downloadUrl, ['sink' => $localFilepath]);
+
+ private function pullCodeFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback = null): void
+ {
+ $isDirty = $this->isLocalGitRepoDirty();
+ if ($isDirty) {
+ throw new AcquiaCliException('Pulling code from your Cloud Platform environment was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.');
+ }
+ // @todo Validate that an Acquia remote is configured for this repository.
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $this->localMachineHelper->execute([
+ 'git',
+ 'fetch',
+ '--all',
+ ], $outputCallback, $this->dir, false);
+ $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback);
+ }
+
+ /**
+ * Checks out the matching branch from a source environment.
+ */
+ private function checkoutBranchFromEnv(EnvironmentResponse $environment, Closure $outputCallback = null): void
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $this->localMachineHelper->execute([
+ 'git',
+ 'checkout',
+ $environment->vcs->path,
+ ], $outputCallback, $this->dir, false);
+ }
+
+ private function doImportRemoteDatabase(
+ string $databaseHost,
+ string $databaseUser,
+ string $databaseName,
+ string $databasePassword,
+ string $localFilepath,
+ Closure $outputCallback = null
+ ): void {
+ $this->dropDbTables($databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback);
+ $this->importDatabaseDump($localFilepath, $databaseHost, $databaseUser, $databaseName, $databasePassword, $outputCallback);
+ $this->localMachineHelper->getFilesystem()->remove($localFilepath);
+ }
+
+ private function downloadDatabaseBackup(
+ EnvironmentResponse $environment,
+ DatabaseResponse $database,
+ BackupResponse $backupResponse,
+ callable $outputCallback = null
+ ): string {
+ if ($outputCallback) {
+ $outputCallback('out', "Downloading backup $backupResponse->id");
+ }
+ $localFilepath = self::getBackupPath($environment, $database, $backupResponse);
+ if ($this->output instanceof ConsoleOutput) {
+ $output = $this->output->section();
+ } else {
+ $output = $this->output;
+ }
+ // These options tell curl to stream the file to disk rather than loading it into memory.
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $acquiaCloudClient->addOption('sink', $localFilepath);
+ $acquiaCloudClient->addOption('curl.options', [
+ 'CURLOPT_FILE' => $localFilepath,
+ 'CURLOPT_RETURNTRANSFER' => false,
+ ]);
+ $acquiaCloudClient->addOption(
+ 'progress',
+ static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $output): void {
+ self::displayDownloadProgress($totalBytes, $downloadedBytes, $progress, $output);
+ }
+ );
+ // This is really just used to allow us to inject values for $url during testing.
+ // It should be empty during normal operations.
+ $url = $this->getBackupDownloadUrl();
+ $acquiaCloudClient->addOption('on_stats', function (TransferStats $stats) use (&$url): void {
+ $url = $stats->getEffectiveUri();
+ });
+
+ try {
+ $acquiaCloudClient->stream(
+ "get",
+ "/environments/$environment->uuid/databases/$database->name/backups/$backupResponse->id/actions/download",
+ $acquiaCloudClient->getOptions()
+ );
return $localFilepath;
- }
- catch (Exception) {
- // Continue in the foreach() loop.
- }
+ } catch (RequestException $exception) {
+ // Deal with broken SSL certificates.
+ // @see https://timi.eu/docs/anatella/5_1_9_1_list_of_curl_error_codes.html
+ if (in_array($exception->getHandlerContext()['errno'], [51, 60], true)) {
+ $outputCallback('out', 'The certificate for ' . $url->getHost() . ' is invalid.');
+ assert($url !== null);
+ $domainsResource = new Domains($this->cloudApiClientService->getClient());
+ $domains = $domainsResource->getAll($environment->uuid);
+ foreach ($domains as $domain) {
+ if ($domain->hostname === $url->getHost()) {
+ continue;
+ }
+ $outputCallback('out', 'Trying alternative host ' . $domain->hostname . ' ');
+ $downloadUrl = $url->withHost($domain->hostname);
+ try {
+ $this->httpClient->request('GET', $downloadUrl, ['sink' => $localFilepath]);
+ return $localFilepath;
+ } catch (Exception) {
+ // Continue in the foreach() loop.
+ }
+ }
+ }
}
- }
- }
- // If we looped through all domains and got here, we didn't download anything.
- throw new AcquiaCliException('Could not download backup');
- }
-
- public function setBackupDownloadUrl(UriInterface $url): void {
- $this->backupDownloadUrl = $url;
- }
-
- private function getBackupDownloadUrl(): ?UriInterface {
- return $this->backupDownloadUrl ?? NULL;
- }
-
- public static function displayDownloadProgress(mixed $totalBytes, mixed $downloadedBytes, mixed &$progress, OutputInterface $output): void {
- if ($totalBytes > 0 && is_null($progress)) {
- $progress = new ProgressBar($output, $totalBytes);
- $progress->setFormat(' %current%/%max% [%bar%] %percent:3s%%');
- $progress->setProgressCharacter('💧');
- $progress->setOverwrite(TRUE);
- $progress->start();
+ // If we looped through all domains and got here, we didn't download anything.
+ throw new AcquiaCliException('Could not download backup');
}
- if (!is_null($progress)) {
- if ($totalBytes === $downloadedBytes && $progress->getProgressPercent() !== 1.0) {
- $progress->finish();
- if ($output instanceof ConsoleSectionOutput) {
- $output->clear();
- }
- return;
- }
- $progress->setProgress($downloadedBytes);
+ public function setBackupDownloadUrl(UriInterface $url): void
+ {
+ $this->backupDownloadUrl = $url;
}
- }
-
- /**
- * Create an on-demand backup and wait for it to become available.
- */
- private function createBackup(EnvironmentResponse $environment, DatabaseResponse $database, Client $acquiaCloudClient): void {
- $backups = new DatabaseBackups($acquiaCloudClient);
- $response = $backups->create($environment->uuid, $database->name);
- $urlParts = explode('/', $response->links->notification->href);
- $notificationUuid = end($urlParts);
- $this->waitForBackup($notificationUuid, $acquiaCloudClient);
- }
-
- /**
- * Wait for an on-demand backup to become available (Cloud API notification).
- *
- * @infection-ignore-all
- */
- protected function waitForBackup(string $notificationUuid, Client $acquiaCloudClient): void {
- $spinnerMessage = 'Waiting for database backup to complete...';
- $successCallback = function (): void {
- $this->output->writeln('');
- $this->output->writeln('Database backup is ready!');
- };
- $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, $spinnerMessage, $successCallback);
- Loop::run();
- }
-
- private function connectToLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = NULL): void {
- if ($outputCallback) {
- $outputCallback('out', "Connecting to database $dbName");
+
+ private function getBackupDownloadUrl(): ?UriInterface
+ {
+ return $this->backupDownloadUrl ?? null;
}
- $this->localMachineHelper->checkRequiredBinariesExist(['mysql']);
- $command = [
- 'mysql',
- '--host',
- $dbHost,
- '--user',
- $dbUser,
- $dbName,
- ];
- $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, NULL, ['MYSQL_PWD' => $dbPassword]);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to connect to local database using credentials mysql://{user}:{password}@{host}/{database}. {message}', [
- 'database' => $dbName,
- 'host' => $dbHost,
- 'message' => $process->getErrorOutput(),
- 'password' => $dbPassword,
- 'user' => $dbUser,
- ]);
+
+ public static function displayDownloadProgress(mixed $totalBytes, mixed $downloadedBytes, mixed &$progress, OutputInterface $output): void
+ {
+ if ($totalBytes > 0 && is_null($progress)) {
+ $progress = new ProgressBar($output, $totalBytes);
+ $progress->setFormat(' %current%/%max% [%bar%] %percent:3s%%');
+ $progress->setProgressCharacter('💧');
+ $progress->setOverwrite(true);
+ $progress->start();
+ }
+
+ if (!is_null($progress)) {
+ if ($totalBytes === $downloadedBytes && $progress->getProgressPercent() !== 1.0) {
+ $progress->finish();
+ if ($output instanceof ConsoleSectionOutput) {
+ $output->clear();
+ }
+ return;
+ }
+ $progress->setProgress($downloadedBytes);
+ }
}
- }
- private function dropDbTables(string $dbHost, string $dbUser, string $dbName, string $dbPassword, ?\Closure $outputCallback = NULL): void {
- if ($outputCallback) {
- $outputCallback('out', "Dropping tables from database $dbName");
+ /**
+ * Create an on-demand backup and wait for it to become available.
+ */
+ private function createBackup(EnvironmentResponse $environment, DatabaseResponse $database, Client $acquiaCloudClient): void
+ {
+ $backups = new DatabaseBackups($acquiaCloudClient);
+ $response = $backups->create($environment->uuid, $database->name);
+ $urlParts = explode('/', $response->links->notification->href);
+ $notificationUuid = end($urlParts);
+ $this->waitForBackup($notificationUuid, $acquiaCloudClient);
+ }
+
+ /**
+ * Wait for an on-demand backup to become available (Cloud API notification).
+ *
+ * @infection-ignore-all
+ */
+ protected function waitForBackup(string $notificationUuid, Client $acquiaCloudClient): void
+ {
+ $spinnerMessage = 'Waiting for database backup to complete...';
+ $successCallback = function (): void {
+ $this->output->writeln('');
+ $this->output->writeln('Database backup is ready!');
+ };
+ $this->waitForNotificationToComplete($acquiaCloudClient, $notificationUuid, $spinnerMessage, $successCallback);
+ Loop::run();
+ }
+
+ private function connectToLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = null): void
+ {
+ if ($outputCallback) {
+ $outputCallback('out', "Connecting to database $dbName");
+ }
+ $this->localMachineHelper->checkRequiredBinariesExist(['mysql']);
+ $command = [
+ 'mysql',
+ '--host',
+ $dbHost,
+ '--user',
+ $dbUser,
+ $dbName,
+ ];
+ $process = $this->localMachineHelper->execute($command, $outputCallback, null, false, null, ['MYSQL_PWD' => $dbPassword]);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to connect to local database using credentials mysql://{user}:{password}@{host}/{database}. {message}', [
+ 'database' => $dbName,
+ 'host' => $dbHost,
+ 'message' => $process->getErrorOutput(),
+ 'password' => $dbPassword,
+ 'user' => $dbUser,
+ ]);
+ }
}
- $this->localMachineHelper->checkRequiredBinariesExist(['mysql']);
- $command = [
- 'mysql',
- '--host',
- $dbHost,
- '--user',
- $dbUser,
- $dbName,
- '--silent',
- '-e',
- 'SHOW TABLES;',
- ];
- $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, NULL, ['MYSQL_PWD' => $dbPassword]);
- $tables = $this->listTablesQuoted($process->getOutput());
- if ($tables) {
- $sql = 'DROP TABLE ' . implode(', ', $tables);
- $tempnam = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli_drop_table_', '.sql');
- $this->localMachineHelper->getFilesystem()->dumpFile($tempnam, $sql);
- $command = [
+
+ private function dropDbTables(string $dbHost, string $dbUser, string $dbName, string $dbPassword, ?\Closure $outputCallback = null): void
+ {
+ if ($outputCallback) {
+ $outputCallback('out', "Dropping tables from database $dbName");
+ }
+ $this->localMachineHelper->checkRequiredBinariesExist(['mysql']);
+ $command = [
'mysql',
'--host',
$dbHost,
'--user',
$dbUser,
$dbName,
+ '--silent',
'-e',
- 'source ' . $tempnam,
- ];
- $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, NULL, ['MYSQL_PWD' => $dbPassword]);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to drop tables from database. {message}', ['message' => $process->getErrorOutput()]);
- }
+ 'SHOW TABLES;',
+ ];
+ $process = $this->localMachineHelper->execute($command, $outputCallback, null, false, null, ['MYSQL_PWD' => $dbPassword]);
+ $tables = $this->listTablesQuoted($process->getOutput());
+ if ($tables) {
+ $sql = 'DROP TABLE ' . implode(', ', $tables);
+ $tempnam = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli_drop_table_', '.sql');
+ $this->localMachineHelper->getFilesystem()->dumpFile($tempnam, $sql);
+ $command = [
+ 'mysql',
+ '--host',
+ $dbHost,
+ '--user',
+ $dbUser,
+ $dbName,
+ '-e',
+ 'source ' . $tempnam,
+ ];
+ $process = $this->localMachineHelper->execute($command, $outputCallback, null, false, null, ['MYSQL_PWD' => $dbPassword]);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to drop tables from database. {message}', ['message' => $process->getErrorOutput()]);
+ }
+ }
}
- }
- private function importDatabaseDump(string $localDumpFilepath, string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = NULL): void {
- if ($outputCallback) {
- $outputCallback('out', "Importing downloaded file to database $dbName");
- }
- $this->logger->debug("Importing $localDumpFilepath to MySQL on local machine");
- $this->localMachineHelper->checkRequiredBinariesExist(['gunzip', 'mysql']);
- if ($this->localMachineHelper->commandExists('pv')) {
- $command = "pv $localDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName";
- }
- else {
- $this->io->warning('Install `pv` to see progress bar');
- $command = "gunzip -c $localDumpFilepath | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName";
- }
+ private function importDatabaseDump(string $localDumpFilepath, string $dbHost, string $dbUser, string $dbName, string $dbPassword, Closure $outputCallback = null): void
+ {
+ if ($outputCallback) {
+ $outputCallback('out', "Importing downloaded file to database $dbName");
+ }
+ $this->logger->debug("Importing $localDumpFilepath to MySQL on local machine");
+ $this->localMachineHelper->checkRequiredBinariesExist(['gunzip', 'mysql']);
+ if ($this->localMachineHelper->commandExists('pv')) {
+ $command = "pv $localDumpFilepath --bytes --rate | gunzip | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName";
+ } else {
+ $this->io->warning('Install `pv` to see progress bar');
+ $command = "gunzip -c $localDumpFilepath | MYSQL_PWD=$dbPassword mysql --host=$dbHost --user=$dbUser $dbName";
+ }
- $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to import local database. {message}', ['message' => $process->getErrorOutput()]);
+ $process = $this->localMachineHelper->executeFromCmd($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to import local database. {message}', ['message' => $process->getErrorOutput()]);
+ }
}
- }
- private function determineSite(string|EnvironmentResponse|array $environment, InputInterface $input): string {
- if (isset($this->site)) {
- return $this->site;
+ private function determineSite(string|EnvironmentResponse|array $environment, InputInterface $input): string
+ {
+ if (isset($this->site)) {
+ return $this->site;
+ }
+
+ if ($input->hasArgument('site') && $input->getArgument('site')) {
+ return $input->getArgument('site');
+ }
+
+ $this->site = $this->promptChooseDrupalSite($environment);
+
+ return $this->site;
}
- if ($input->hasArgument('site') && $input->getArgument('site')) {
- return $input->getArgument('site');
+ private function rsyncFilesFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback, string $site): void
+ {
+ $sourceDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site);
+ $destinationDir = $this->getLocalFilesDir($site);
+ $this->localMachineHelper->getFilesystem()->mkdir($destinationDir);
+
+ $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback);
}
- $this->site = $this->promptChooseDrupalSite($environment);
+ protected function determineCloneProject(OutputInterface $output): bool
+ {
+ $finder = $this->localMachineHelper->getFinder()->files()->in($this->dir)->ignoreDotFiles(false);
- return $this->site;
- }
+ // If we are in an IDE, assume we should pull into /home/ide/project.
+ if ($this->dir === '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$finder->hasResults()) {
+ $output->writeln('Cloning into current directory.');
+ return true;
+ }
- private function rsyncFilesFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback, string $site): void {
- $sourceDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site);
- $destinationDir = $this->getLocalFilesDir($site);
- $this->localMachineHelper->getFilesystem()->mkdir($destinationDir);
+ // If $this->projectDir is set, pull into that dir rather than cloning.
+ if ($this->projectDir) {
+ return false;
+ }
- $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback);
- }
+ // If ./.git exists, assume we pull into that dir rather than cloning.
+ if (file_exists(Path::join($this->dir, '.git'))) {
+ return false;
+ }
+ $output->writeln('Could not find a git repository in the current directory');
- protected function determineCloneProject(OutputInterface $output): bool {
- $finder = $this->localMachineHelper->getFinder()->files()->in($this->dir)->ignoreDotFiles(FALSE);
+ if (!$finder->hasResults() && $this->io->confirm('Would you like to clone a project into the current directory?')) {
+ return true;
+ }
- // If we are in an IDE, assume we should pull into /home/ide/project.
- if ($this->dir === '/home/ide/project' && AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$finder->hasResults()) {
- $output->writeln('Cloning into current directory.');
- return TRUE;
- }
+ $output->writeln('Could not clone into the current directory because it is not empty');
- // If $this->projectDir is set, pull into that dir rather than cloning.
- if ($this->projectDir) {
- return FALSE;
+ throw new AcquiaCliException('Execute this command from within a Drupal project directory or an empty directory');
}
- // If ./.git exists, assume we pull into that dir rather than cloning.
- if (file_exists(Path::join($this->dir, '.git'))) {
- return FALSE;
+ private function cloneFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback): void
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $command = [
+ 'git',
+ 'clone',
+ $chosenEnvironment->vcs->url,
+ $this->dir,
+ ];
+ $process = $this->localMachineHelper->execute($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL), null, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=no']);
+ $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]);
+ }
+ $this->projectDir = $this->dir;
}
- $output->writeln('Could not find a git repository in the current directory');
- if (!$finder->hasResults() && $this->io->confirm('Would you like to clone a project into the current directory?')) {
- return TRUE;
+ protected function checkEnvironmentPhpVersions(EnvironmentResponse $environment): void
+ {
+ $version = $this->getIdePhpVersion();
+ if (empty($version)) {
+ $this->io->warning("Could not determine current PHP version. Set it by running acli ide:php-version.");
+ } elseif (!$this->environmentPhpVersionMatches($environment)) {
+ $this->io->warning("You are using PHP version $version but the upstream environment $environment->label is using PHP version {$environment->configuration->php->version}");
+ }
}
- $output->writeln('Could not clone into the current directory because it is not empty');
-
- throw new AcquiaCliException('Execute this command from within a Drupal project directory or an empty directory');
- }
-
- private function cloneFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback): void {
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $command = [
- 'git',
- 'clone',
- $chosenEnvironment->vcs->url,
- $this->dir,
- ];
- $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL), NULL, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=no']);
- $this->checkoutBranchFromEnv($chosenEnvironment, $outputCallback);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]);
+ protected function matchIdePhpVersion(
+ OutputInterface $output,
+ EnvironmentResponse $chosenEnvironment
+ ): void {
+ if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->environmentPhpVersionMatches($chosenEnvironment)) {
+ $answer = $this->io->confirm("Would you like to change the PHP version on this IDE to match the PHP version on the $chosenEnvironment->label ({$chosenEnvironment->configuration->php->version})> environment?", false);
+ if ($answer) {
+ $command = $this->getApplication()->find('ide:php-version');
+ $command->run(
+ new ArrayInput(['command' => 'ide:php-version', 'version' => $chosenEnvironment->configuration->php->version]),
+ $output
+ );
+ }
+ }
}
- $this->projectDir = $this->dir;
- }
- protected function checkEnvironmentPhpVersions(EnvironmentResponse $environment): void {
- $version = $this->getIdePhpVersion();
- if (empty($version)) {
- $this->io->warning("Could not determine current PHP version. Set it by running acli ide:php-version.");
- }
- else if (!$this->environmentPhpVersionMatches($environment)) {
- $this->io->warning("You are using PHP version $version but the upstream environment $environment->label is using PHP version {$environment->configuration->php->version}");
- }
- }
-
- protected function matchIdePhpVersion(
- OutputInterface $output,
- EnvironmentResponse $chosenEnvironment
- ): void {
- if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->environmentPhpVersionMatches($chosenEnvironment)) {
- $answer = $this->io->confirm("Would you like to change the PHP version on this IDE to match the PHP version on the $chosenEnvironment->label ({$chosenEnvironment->configuration->php->version})> environment?", FALSE);
- if ($answer) {
- $command = $this->getApplication()->find('ide:php-version');
- $command->run(new ArrayInput(['command' => 'ide:php-version', 'version' => $chosenEnvironment->configuration->php->version]),
- $output);
- }
- }
- }
-
- private function environmentPhpVersionMatches(EnvironmentResponse $environment): bool {
- $currentPhpVersion = $this->getIdePhpVersion();
- return $environment->configuration->php->version === $currentPhpVersion;
- }
-
- private function getDatabaseBackup(
- Client $acquiaCloudClient,
- string|EnvironmentResponse|array $environment,
- DatabaseResponse $database
- ): BackupResponse {
- $databaseBackups = new DatabaseBackups($acquiaCloudClient);
- $backupsResponse = $databaseBackups->getAll($environment->uuid, $database->name);
- if (!count($backupsResponse)) {
- $this->io->warning('No existing backups found, creating an on-demand backup now. This will take some time depending on the size of the database.');
- $this->createBackup($environment, $database, $acquiaCloudClient);
- $backupsResponse = $databaseBackups->getAll($environment->uuid,
- $database->name);
- }
- $backupResponse = $backupsResponse[0];
- $this->logger->debug('Using database backup (id #' . $backupResponse->id . ') generated at ' . $backupResponse->completedAt);
-
- return $backupResponse;
- }
-
- /**
- * Print information to the console about the selected database backup.
- */
- private function printDatabaseBackupInfo(
- BackupResponse $backupResponse,
- EnvironmentResponse $sourceEnvironment
- ): void {
- $interval = time() - strtotime($backupResponse->completedAt);
- $hoursInterval = floor($interval / 60 / 60);
- $dateFormatted = date("D M j G:i:s T Y", strtotime($backupResponse->completedAt));
- $webLink = "https://cloud.acquia.com/a/environments/{$sourceEnvironment->uuid}/databases";
- $messages = [
- "Using a database backup that is $hoursInterval hours old. Backup #$backupResponse->id was created at {$dateFormatted}.",
- "You can view your backups here: $webLink",
- "To generate a new backup, re-run this command with the --on-demand option.",
- ];
- if ($hoursInterval > 24) {
- $this->io->warning($messages);
- }
- else {
- $this->io->info($messages);
+ private function environmentPhpVersionMatches(EnvironmentResponse $environment): bool
+ {
+ $currentPhpVersion = $this->getIdePhpVersion();
+ return $environment->configuration->php->version === $currentPhpVersion;
+ }
+
+ private function getDatabaseBackup(
+ Client $acquiaCloudClient,
+ string|EnvironmentResponse|array $environment,
+ DatabaseResponse $database
+ ): BackupResponse {
+ $databaseBackups = new DatabaseBackups($acquiaCloudClient);
+ $backupsResponse = $databaseBackups->getAll($environment->uuid, $database->name);
+ if (!count($backupsResponse)) {
+ $this->io->warning('No existing backups found, creating an on-demand backup now. This will take some time depending on the size of the database.');
+ $this->createBackup($environment, $database, $acquiaCloudClient);
+ $backupsResponse = $databaseBackups->getAll(
+ $environment->uuid,
+ $database->name
+ );
+ }
+ $backupResponse = $backupsResponse[0];
+ $this->logger->debug('Using database backup (id #' . $backupResponse->id . ') generated at ' . $backupResponse->completedAt);
+
+ return $backupResponse;
+ }
+
+ /**
+ * Print information to the console about the selected database backup.
+ */
+ private function printDatabaseBackupInfo(
+ BackupResponse $backupResponse,
+ EnvironmentResponse $sourceEnvironment
+ ): void {
+ $interval = time() - strtotime($backupResponse->completedAt);
+ $hoursInterval = floor($interval / 60 / 60);
+ $dateFormatted = date("D M j G:i:s T Y", strtotime($backupResponse->completedAt));
+ $webLink = "https://cloud.acquia.com/a/environments/{$sourceEnvironment->uuid}/databases";
+ $messages = [
+ "Using a database backup that is $hoursInterval hours old. Backup #$backupResponse->id was created at {$dateFormatted}.",
+ "You can view your backups here: $webLink",
+ "To generate a new backup, re-run this command with the --on-demand option.",
+ ];
+ if ($hoursInterval > 24) {
+ $this->io->warning($messages);
+ } else {
+ $this->io->info($messages);
+ }
}
- }
- private function importRemoteDatabase(DatabaseResponse $database, string $localFilepath, Closure $outputCallback = NULL): void {
- if ($database->flags->default) {
- // Easy case, import the default db into the default db.
- $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback);
- }
- else if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !getenv('IDE_ENABLE_MULTISITE')) {
- // Import non-default db into default db. Needed on legacy IDE without multiple dbs.
- // @todo remove this case once all IDEs support multiple dbs.
- $this->io->note("Cloud IDE only supports importing into the default Drupal database. Acquia CLI will import the NON-DEFAULT database {$database->name} into the DEFAULT database {$this->getLocalDbName()}");
- $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback);
- }
- else {
- // Import non-default db into non-default db.
- $this->io->note("Acquia CLI assumes that the local name for the {$database->name} database is also {$database->name}");
- if (AcquiaDrupalEnvironmentDetector::isLandoEnv() || AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
- $this->doImportRemoteDatabase($this->getLocalDbHost(), 'root', $database->name, '', $localFilepath, $outputCallback);
- }
- else {
- $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $database->name, $this->getLocalDbPassword(), $localFilepath, $outputCallback);
- }
+ private function importRemoteDatabase(DatabaseResponse $database, string $localFilepath, Closure $outputCallback = null): void
+ {
+ if ($database->flags->default) {
+ // Easy case, import the default db into the default db.
+ $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback);
+ } elseif (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !getenv('IDE_ENABLE_MULTISITE')) {
+ // Import non-default db into default db. Needed on legacy IDE without multiple dbs.
+ // @todo remove this case once all IDEs support multiple dbs.
+ $this->io->note("Cloud IDE only supports importing into the default Drupal database. Acquia CLI will import the NON-DEFAULT database {$database->name} into the DEFAULT database {$this->getLocalDbName()}");
+ $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $localFilepath, $outputCallback);
+ } else {
+ // Import non-default db into non-default db.
+ $this->io->note("Acquia CLI assumes that the local name for the {$database->name} database is also {$database->name}");
+ if (AcquiaDrupalEnvironmentDetector::isLandoEnv() || AcquiaDrupalEnvironmentDetector::isAhIdeEnv()) {
+ $this->doImportRemoteDatabase($this->getLocalDbHost(), 'root', $database->name, '', $localFilepath, $outputCallback);
+ } else {
+ $this->doImportRemoteDatabase($this->getLocalDbHost(), $this->getLocalDbUser(), $database->name, $this->getLocalDbPassword(), $localFilepath, $outputCallback);
+ }
+ }
}
- }
-
}
diff --git a/src/Command/Pull/PullDatabaseCommand.php b/src/Command/Pull/PullDatabaseCommand.php
index 15fe38a55..19514c15e 100644
--- a/src/Command/Pull/PullDatabaseCommand.php
+++ b/src/Command/Pull/PullDatabaseCommand.php
@@ -1,6 +1,6 @@
setHelp('This uses the latest available database backup, which may be up to 24 hours old. If no backup exists, one will be created.')
- ->acceptEnvironmentId()
- ->acceptSite()
- ->addOption('no-scripts', NULL, InputOption::VALUE_NONE,
- 'Do not run any additional scripts after the database is pulled. E.g., drush cache-rebuild, drush sql-sanitize, etc.')
- ->addOption('on-demand', NULL, InputOption::VALUE_NONE,
- 'Force creation of an on-demand backup. This takes much longer than using an existing backup (when one is available)')
- ->addOption('no-import', NULL, InputOption::VALUE_NONE,
- 'Download the backup but do not import it (implies --no-scripts)')
- ->addOption('multiple-dbs', NULL, InputOption::VALUE_NONE,
- 'Download multiple dbs. Defaults to FALSE.');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $noScripts = $input->hasOption('no-scripts') && $input->getOption('no-scripts');
- $onDemand = $input->hasOption('on-demand') && $input->getOption('on-demand');
- $noImport = $input->hasOption('no-import') && $input->getOption('no-import');
- $multipleDbs = $input->hasOption('multiple-dbs') && $input->getOption('multiple-dbs');
- // $noImport implies $noScripts.
- $noScripts = $noImport || $noScripts;
- $this->setDirAndRequireProjectCwd($input);
- $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE);
- $this->pullDatabase($input, $output, $sourceEnvironment, $onDemand, $noImport, $multipleDbs);
- if (!$noScripts) {
- $this->runDrushCacheClear($this->getOutputCallback($output, $this->checklist), $this->checklist);
- $this->runDrushSqlSanitize($this->getOutputCallback($output, $this->checklist), $this->checklist);
+final class PullDatabaseCommand extends PullCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->setHelp('This uses the latest available database backup, which may be up to 24 hours old. If no backup exists, one will be created.')
+ ->acceptEnvironmentId()
+ ->acceptSite()
+ ->addOption(
+ 'no-scripts',
+ null,
+ InputOption::VALUE_NONE,
+ 'Do not run any additional scripts after the database is pulled. E.g., drush cache-rebuild, drush sql-sanitize, etc.'
+ )
+ ->addOption(
+ 'on-demand',
+ null,
+ InputOption::VALUE_NONE,
+ 'Force creation of an on-demand backup. This takes much longer than using an existing backup (when one is available)'
+ )
+ ->addOption(
+ 'no-import',
+ null,
+ InputOption::VALUE_NONE,
+ 'Download the backup but do not import it (implies --no-scripts)'
+ )
+ ->addOption(
+ 'multiple-dbs',
+ null,
+ InputOption::VALUE_NONE,
+ 'Download multiple dbs. Defaults to FALSE.'
+ );
}
- return Command::SUCCESS;
- }
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $noScripts = $input->hasOption('no-scripts') && $input->getOption('no-scripts');
+ $onDemand = $input->hasOption('on-demand') && $input->getOption('on-demand');
+ $noImport = $input->hasOption('no-import') && $input->getOption('no-import');
+ $multipleDbs = $input->hasOption('multiple-dbs') && $input->getOption('multiple-dbs');
+ // $noImport implies $noScripts.
+ $noScripts = $noImport || $noScripts;
+ $this->setDirAndRequireProjectCwd($input);
+ $sourceEnvironment = $this->determineEnvironment($input, $output, true);
+ $this->pullDatabase($input, $output, $sourceEnvironment, $onDemand, $noImport, $multipleDbs);
+ if (!$noScripts) {
+ $this->runDrushCacheClear($this->getOutputCallback($output, $this->checklist), $this->checklist);
+ $this->runDrushSqlSanitize($this->getOutputCallback($output, $this->checklist), $this->checklist);
+ }
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Pull/PullFilesCommand.php b/src/Command/Pull/PullFilesCommand.php
index abf72f9d3..a42eea055 100644
--- a/src/Command/Pull/PullFilesCommand.php
+++ b/src/Command/Pull/PullFilesCommand.php
@@ -1,6 +1,6 @@
acceptEnvironmentId()
- ->acceptSite();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->setDirAndRequireProjectCwd($input);
- $sourceEnvironment = $this->determineEnvironment($input, $output, TRUE);
- $this->pullFiles($input, $output, $sourceEnvironment);
-
- return Command::SUCCESS;
- }
-
+final class PullFilesCommand extends PullCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId()
+ ->acceptSite();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ $sourceEnvironment = $this->determineEnvironment($input, $output, true);
+ $this->pullFiles($input, $output, $sourceEnvironment);
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Pull/PullScriptsCommand.php b/src/Command/Pull/PullScriptsCommand.php
index 7ff3f3018..4ab34d089 100644
--- a/src/Command/Pull/PullScriptsCommand.php
+++ b/src/Command/Pull/PullScriptsCommand.php
@@ -1,6 +1,6 @@
acceptEnvironmentId()
- ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed');
- }
-
- protected function initialize(InputInterface $input, OutputInterface $output): void {
- parent::initialize($input, $output);
- $this->checklist = new Checklist($output);
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->setDirAndRequireProjectCwd($input);
- $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist);
-
- return Command::SUCCESS;
- }
-
+final class PullScriptsCommand extends CommandBase
+{
+ protected Checklist $checklist;
+
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId()
+ ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be refreshed');
+ }
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ parent::initialize($input, $output);
+ $this->checklist = new Checklist($output);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ $this->executeAllScripts($this->getOutputCallback($output, $this->checklist), $this->checklist);
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Push/PushArtifactCommand.php b/src/Command/Push/PushArtifactCommand.php
index 556f59bfa..afb0a4408 100644
--- a/src/Command/Push/PushArtifactCommand.php
+++ b/src/Command/Push/PushArtifactCommand.php
@@ -1,6 +1,6 @@
- */
- protected array $vendorDirs;
-
- /**
- * Composer scaffold files.
- *
- * @var array
- */
- protected array $scaffoldFiles;
-
- private string $composerJsonPath;
-
- private string $docrootPath;
-
- private string $destinationGitRef;
-
- protected Checklist $checklist;
-
- protected function configure(): void {
- $this
- ->addOption('dir', NULL, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be pushed')
- ->addOption('no-sanitize', NULL, InputOption::VALUE_NONE, 'Do not sanitize the build artifact')
- ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Deprecated: Use no-push instead')
- ->addOption('no-push', NULL, InputOption::VALUE_NONE, 'Do not push changes to Acquia Cloud')
- ->addOption('no-commit', NULL, InputOption::VALUE_NONE, 'Do not commit changes. Implies no-push')
- ->addOption('no-clone', NULL, InputOption::VALUE_NONE, 'Do not clone repository. Implies no-commit and no-push')
- ->addOption('destination-git-urls', 'u', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The URL(s) of your git repository to which the artifact branch will be pushed')
- ->addOption('destination-git-branch', 'b', InputOption::VALUE_REQUIRED, 'The destination branch to push the artifact to')
- ->addOption('destination-git-tag', 't', InputOption::VALUE_REQUIRED, 'The destination tag to push the artifact to. Using this option requires also using the --source-git-tag option')
- ->addOption('source-git-tag', 's', InputOption::VALUE_REQUIRED, 'The source tag from which to create the tag artifact')
- ->acceptEnvironmentId()
- ->setHelp('This command builds a sanitized deploy artifact by running composer install>, removing sensitive files, and committing vendor directories.' . PHP_EOL . PHP_EOL
- . 'Vendor directories and scaffold files are committed to the build artifact even if they are ignored in the source repository.' . PHP_EOL . PHP_EOL
- . 'To run additional build or sanitization steps (e.g. npm install>), add a post-install-cmd> script to your composer.json> file: https://getcomposer.org/doc/articles/scripts.md#command-events')
- ->addUsage('--destination-git-branch=main-build')
- ->addUsage('--source-git-tag=foo-build --destination-git-tag=1.0.0')
- ->addUsage('--destination-git-urls=example@svn-1.prod.hosting.acquia.com:example.git --destination-git-branch=main-build');
- }
-
- protected function initialize(InputInterface $input, OutputInterface $output): void {
- parent::initialize($input, $output);
- $this->checklist = new Checklist($output);
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->setDirAndRequireProjectCwd($input);
- if ($input->getOption('no-clone')) {
- $input->setOption('no-commit', TRUE);
- }
- if ($input->getOption('no-commit')) {
- $input->setOption('no-push', TRUE);
- }
- $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact');
- $this->composerJsonPath = Path::join($this->dir, 'composer.json');
- $this->docrootPath = Path::join($this->dir, 'docroot');
- $this->validateSourceCode();
-
- $isDirty = $this->isLocalGitRepoDirty();
- $commitHash = $this->getLocalGitCommitHash();
- if ($isDirty) {
- throw new AcquiaCliException('Pushing code was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.');
- }
- $this->checklist = new Checklist($output);
- $outputCallback = $this->getOutputCallback($output, $this->checklist);
-
- $destinationGitUrls = [];
- $destinationGitRef = '';
- if (!$input->getOption('no-clone')) {
- $applicationUuid = $this->determineCloudApplication();
- $destinationGitUrls = $this->determineDestinationGitUrls($applicationUuid);
- $destinationGitRef = $this->determineDestinationGitRef();
- $sourceGitBranch = $this->determineSourceGitRef();
- $destinationGitUrlsString = implode(',', $destinationGitUrls);
- $refType = $this->input->getOption('destination-git-tag') ? 'tag' : 'branch';
- $this->io->note([
- "Acquia CLI will:",
- "- git clone $sourceGitBranch from $destinationGitUrls[0]",
- "- Compile the contents of $this->dir into an artifact in a temporary directory",
- "- Copy the artifact files into the checked out copy of $sourceGitBranch",
- "- Commit changes and push the $destinationGitRef $refType to the following git remote(s):",
- " $destinationGitUrlsString",
- ]);
-
- $this->checklist->addItem('Preparing artifact directory');
- $this->cloneSourceBranch($outputCallback, $artifactDir, $destinationGitUrls[0], $sourceGitBranch);
- $this->checklist->completePreviousItem();
- }
-
- $this->checklist->addItem('Generating build artifact');
- $this->buildArtifact($outputCallback, $artifactDir);
- $this->checklist->completePreviousItem();
-
- if (!$input->getOption('no-sanitize')) {
- $this->checklist->addItem('Sanitizing build artifact');
- $this->sanitizeArtifact($outputCallback, $artifactDir);
- $this->checklist->completePreviousItem();
- }
-
- if (!$input->getOption('no-commit')) {
- $this->checklist->addItem("Committing changes (commit hash: $commitHash)");
- $this->commit($outputCallback, $artifactDir, $commitHash);
- $this->checklist->completePreviousItem();
- }
-
- if (!$input->getOption('dry-run') && !$input->getOption('no-push')) {
- if ($tagName = $input->getOption('destination-git-tag')) {
- $this->checklist->addItem("Creating $tagName> tag.");
- $this->createTag($tagName, $outputCallback, $artifactDir);
+final class PushArtifactCommand extends CommandBase
+{
+ /**
+ * Composer vendor directories.
+ *
+ * @var array
+ */
+ protected array $vendorDirs;
+
+ /**
+ * Composer scaffold files.
+ *
+ * @var array
+ */
+ protected array $scaffoldFiles;
+
+ private string $composerJsonPath;
+
+ private string $docrootPath;
+
+ private string $destinationGitRef;
+
+ protected Checklist $checklist;
+
+ protected function configure(): void
+ {
+ $this
+ ->addOption('dir', null, InputArgument::OPTIONAL, 'The directory containing the Drupal project to be pushed')
+ ->addOption('no-sanitize', null, InputOption::VALUE_NONE, 'Do not sanitize the build artifact')
+ ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Deprecated: Use no-push instead')
+ ->addOption('no-push', null, InputOption::VALUE_NONE, 'Do not push changes to Acquia Cloud')
+ ->addOption('no-commit', null, InputOption::VALUE_NONE, 'Do not commit changes. Implies no-push')
+ ->addOption('no-clone', null, InputOption::VALUE_NONE, 'Do not clone repository. Implies no-commit and no-push')
+ ->addOption('destination-git-urls', 'u', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The URL(s) of your git repository to which the artifact branch will be pushed')
+ ->addOption('destination-git-branch', 'b', InputOption::VALUE_REQUIRED, 'The destination branch to push the artifact to')
+ ->addOption('destination-git-tag', 't', InputOption::VALUE_REQUIRED, 'The destination tag to push the artifact to. Using this option requires also using the --source-git-tag option')
+ ->addOption('source-git-tag', 's', InputOption::VALUE_REQUIRED, 'The source tag from which to create the tag artifact')
+ ->acceptEnvironmentId()
+ ->setHelp('This command builds a sanitized deploy artifact by running composer install>, removing sensitive files, and committing vendor directories.' . PHP_EOL . PHP_EOL
+ . 'Vendor directories and scaffold files are committed to the build artifact even if they are ignored in the source repository.' . PHP_EOL . PHP_EOL
+ . 'To run additional build or sanitization steps (e.g. npm install>), add a post-install-cmd> script to your composer.json> file: https://getcomposer.org/doc/articles/scripts.md#command-events')
+ ->addUsage('--destination-git-branch=main-build')
+ ->addUsage('--source-git-tag=foo-build --destination-git-tag=1.0.0')
+ ->addUsage('--destination-git-urls=example@svn-1.prod.hosting.acquia.com:example.git --destination-git-branch=main-build');
+ }
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ parent::initialize($input, $output);
+ $this->checklist = new Checklist($output);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ if ($input->getOption('no-clone')) {
+ $input->setOption('no-commit', true);
+ }
+ if ($input->getOption('no-commit')) {
+ $input->setOption('no-push', true);
+ }
+ $artifactDir = Path::join(sys_get_temp_dir(), 'acli-push-artifact');
+ $this->composerJsonPath = Path::join($this->dir, 'composer.json');
+ $this->docrootPath = Path::join($this->dir, 'docroot');
+ $this->validateSourceCode();
+
+ $isDirty = $this->isLocalGitRepoDirty();
+ $commitHash = $this->getLocalGitCommitHash();
+ if ($isDirty) {
+ throw new AcquiaCliException('Pushing code was aborted because your local Git repository has uncommitted changes. Either commit, reset, or stash your changes via git.');
+ }
+ $this->checklist = new Checklist($output);
+ $outputCallback = $this->getOutputCallback($output, $this->checklist);
+
+ $destinationGitUrls = [];
+ $destinationGitRef = '';
+ if (!$input->getOption('no-clone')) {
+ $applicationUuid = $this->determineCloudApplication();
+ $destinationGitUrls = $this->determineDestinationGitUrls($applicationUuid);
+ $destinationGitRef = $this->determineDestinationGitRef();
+ $sourceGitBranch = $this->determineSourceGitRef();
+ $destinationGitUrlsString = implode(',', $destinationGitUrls);
+ $refType = $this->input->getOption('destination-git-tag') ? 'tag' : 'branch';
+ $this->io->note([
+ "Acquia CLI will:",
+ "- git clone $sourceGitBranch from $destinationGitUrls[0]",
+ "- Compile the contents of $this->dir into an artifact in a temporary directory",
+ "- Copy the artifact files into the checked out copy of $sourceGitBranch",
+ "- Commit changes and push the $destinationGitRef $refType to the following git remote(s):",
+ " $destinationGitUrlsString",
+ ]);
+
+ $this->checklist->addItem('Preparing artifact directory');
+ $this->cloneSourceBranch($outputCallback, $artifactDir, $destinationGitUrls[0], $sourceGitBranch);
+ $this->checklist->completePreviousItem();
+ }
+
+ $this->checklist->addItem('Generating build artifact');
+ $this->buildArtifact($outputCallback, $artifactDir);
$this->checklist->completePreviousItem();
- $this->checklist->addItem("Pushing changes to $tagName> tag.");
- $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $tagName);
- }
- else {
- $this->checklist->addItem("Pushing changes to $destinationGitRef> branch.");
- $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $destinationGitRef . ':' . $destinationGitRef);
- }
- $this->checklist->completePreviousItem();
- }
- else {
- $this->logger->warning("The --dry-run> (deprecated) or --no-push> option prevented changes from being pushed to Acquia Cloud. The artifact has been built at $artifactDir>");
- }
-
- return Command::SUCCESS;
- }
-
- /**
- * @return string[]
- */
- private function determineDestinationGitUrls(?string $applicationUuid): array {
- if ($this->input->getOption('destination-git-urls')) {
- return $this->input->getOption('destination-git-urls');
- }
- if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_URLS')) {
- return explode(',', $envVar);
- }
- if ($this->datastoreAcli->get('push.artifact.destination-git-urls')) {
- return $this->datastoreAcli->get('push.artifact.destination-git-urls');
- }
- return [$this->getAnyVcsUrl($applicationUuid)];
- }
-
- /**
- * Prepare a directory to build the artifact.
- */
- private function cloneSourceBranch(Closure $outputCallback, string $artifactDir, string $vcsUrl, string $vcsPath): void {
- $fs = $this->localMachineHelper->getFilesystem();
-
- $outputCallback('out', "Removing $artifactDir if it exists");
- $fs->remove($artifactDir);
-
- $outputCallback('out', "Initializing Git in $artifactDir");
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $process = $this->localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]);
- }
- $process = $this->localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- // Remote branch does not exist. Just create it locally. This will create
- // the new branch off of the current commit.
- $process = $this->localMachineHelper->execute(['git', 'checkout', '-b', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- }
- else {
- $process = $this->localMachineHelper->execute(['git', 'checkout', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- }
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Could not checkout $vcsPath branch locally: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]);
- }
-
- $outputCallback('out', 'Global .gitignore file is temporarily disabled during artifact builds.');
- $this->localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- $this->localMachineHelper->execute(['git', 'config', '--local', 'core.fileMode', 'true'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
-
- // Vendor directories can be "corrupt" (i.e. missing scaffold files due to earlier sanitization) in ways that break composer install.
- $outputCallback('out', 'Removing vendor directories');
- foreach ($this->vendorDirs() as $vendorDirectory) {
- $fs->remove(Path::join($artifactDir, $vendorDirectory));
- }
- }
-
- /**
- * Build the artifact.
- */
- private function buildArtifact(Closure $outputCallback, string $artifactDir): void {
- // @todo generate a deploy identifier
- // @see https://git.drupalcode.org/project/drupal/-/blob/9.1.x/sites/default/default.settings.php#L295
- $outputCallback('out', "Mirroring source files from $this->dir to $artifactDir");
- $originFinder = $this->localMachineHelper->getFinder();
- $originFinder->in($this->dir)
- // Include dot files like .htaccess.
- ->ignoreDotFiles(FALSE)
- // Ignore VCS ignored files (e.g. vendor) to speed up the mirror (Composer will restore them later).
- ->ignoreVCSIgnored(TRUE);
- $targetFinder = $this->localMachineHelper->getFinder();
- $targetFinder->in($artifactDir)->ignoreDotFiles(FALSE);
- $this->localMachineHelper->getFilesystem()->remove($targetFinder);
- $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => TRUE]);
-
- $this->localMachineHelper->checkRequiredBinariesExist(['composer']);
- $outputCallback('out', 'Installing Composer production dependencies');
- $process = $this->localMachineHelper->execute(['composer', 'install', '--no-dev', '--no-interaction', '--optimize-autoloader'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Unable to install composer dependencies: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]);
- }
- }
-
- /**
- * Sanitize the artifact.
- */
- private function sanitizeArtifact(Closure $outputCallback, string $artifactDir): void {
- $outputCallback('out', 'Finding Drupal core text files');
- $sanitizeFinder = $this->localMachineHelper->getFinder()
- ->files()
- ->name('*.txt')
- ->notName('LICENSE.txt')
- ->in("$artifactDir/docroot/core");
-
- $outputCallback('out', 'Finding VCS directories');
- $vcsFinder = $this->localMachineHelper->getFinder()
- ->ignoreDotFiles(FALSE)
- ->ignoreVCS(FALSE)
- ->directories()
- ->in(["$artifactDir/docroot",
+ if (!$input->getOption('no-sanitize')) {
+ $this->checklist->addItem('Sanitizing build artifact');
+ $this->sanitizeArtifact($outputCallback, $artifactDir);
+ $this->checklist->completePreviousItem();
+ }
+
+ if (!$input->getOption('no-commit')) {
+ $this->checklist->addItem("Committing changes (commit hash: $commitHash)");
+ $this->commit($outputCallback, $artifactDir, $commitHash);
+ $this->checklist->completePreviousItem();
+ }
+
+ if (!$input->getOption('dry-run') && !$input->getOption('no-push')) {
+ if ($tagName = $input->getOption('destination-git-tag')) {
+ $this->checklist->addItem("Creating $tagName> tag.");
+ $this->createTag($tagName, $outputCallback, $artifactDir);
+ $this->checklist->completePreviousItem();
+ $this->checklist->addItem("Pushing changes to $tagName> tag.");
+ $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $tagName);
+ } else {
+ $this->checklist->addItem("Pushing changes to $destinationGitRef> branch.");
+ $this->pushArtifact($outputCallback, $artifactDir, $destinationGitUrls, $destinationGitRef . ':' . $destinationGitRef);
+ }
+ $this->checklist->completePreviousItem();
+ } else {
+ $this->logger->warning("The --dry-run> (deprecated) or --no-push> option prevented changes from being pushed to Acquia Cloud. The artifact has been built at $artifactDir>");
+ }
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function determineDestinationGitUrls(?string $applicationUuid): array
+ {
+ if ($this->input->getOption('destination-git-urls')) {
+ return $this->input->getOption('destination-git-urls');
+ }
+ if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_URLS')) {
+ return explode(',', $envVar);
+ }
+ if ($this->datastoreAcli->get('push.artifact.destination-git-urls')) {
+ return $this->datastoreAcli->get('push.artifact.destination-git-urls');
+ }
+
+ return [$this->getAnyVcsUrl($applicationUuid)];
+ }
+
+ /**
+ * Prepare a directory to build the artifact.
+ */
+ private function cloneSourceBranch(Closure $outputCallback, string $artifactDir, string $vcsUrl, string $vcsPath): void
+ {
+ $fs = $this->localMachineHelper->getFilesystem();
+
+ $outputCallback('out', "Removing $artifactDir if it exists");
+ $fs->remove($artifactDir);
+
+ $outputCallback('out', "Initializing Git in $artifactDir");
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $process = $this->localMachineHelper->execute(['git', 'clone', '--depth=1', $vcsUrl, $artifactDir], $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Failed to clone repository from the Cloud Platform: {message}', ['message' => $process->getErrorOutput()]);
+ }
+ $process = $this->localMachineHelper->execute(['git', 'fetch', '--depth=1', $vcsUrl, $vcsPath . ':' . $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ // Remote branch does not exist. Just create it locally. This will create
+ // the new branch off of the current commit.
+ $process = $this->localMachineHelper->execute(['git', 'checkout', '-b', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ } else {
+ $process = $this->localMachineHelper->execute(['git', 'checkout', $vcsPath], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ }
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Could not checkout $vcsPath branch locally: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]);
+ }
+
+ $outputCallback('out', 'Global .gitignore file is temporarily disabled during artifact builds.');
+ $this->localMachineHelper->execute(['git', 'config', '--local', 'core.excludesFile', 'false'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ $this->localMachineHelper->execute(['git', 'config', '--local', 'core.fileMode', 'true'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+
+ // Vendor directories can be "corrupt" (i.e. missing scaffold files due to earlier sanitization) in ways that break composer install.
+ $outputCallback('out', 'Removing vendor directories');
+ foreach ($this->vendorDirs() as $vendorDirectory) {
+ $fs->remove(Path::join($artifactDir, $vendorDirectory));
+ }
+ }
+
+ /**
+ * Build the artifact.
+ */
+ private function buildArtifact(Closure $outputCallback, string $artifactDir): void
+ {
+ // @todo generate a deploy identifier
+ // @see https://git.drupalcode.org/project/drupal/-/blob/9.1.x/sites/default/default.settings.php#L295
+ $outputCallback('out', "Mirroring source files from $this->dir to $artifactDir");
+ $originFinder = $this->localMachineHelper->getFinder();
+ $originFinder->in($this->dir)
+ // Include dot files like .htaccess.
+ ->ignoreDotFiles(false)
+ // Ignore VCS ignored files (e.g. vendor) to speed up the mirror (Composer will restore them later).
+ ->ignoreVCSIgnored(true);
+ $targetFinder = $this->localMachineHelper->getFinder();
+ $targetFinder->in($artifactDir)->ignoreDotFiles(false);
+ $this->localMachineHelper->getFilesystem()->remove($targetFinder);
+ $this->localMachineHelper->getFilesystem()->mirror($this->dir, $artifactDir, $originFinder, ['override' => true]);
+
+ $this->localMachineHelper->checkRequiredBinariesExist(['composer']);
+ $outputCallback('out', 'Installing Composer production dependencies');
+ $process = $this->localMachineHelper->execute(['composer', 'install', '--no-dev', '--no-interaction', '--optimize-autoloader'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Unable to install composer dependencies: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]);
+ }
+ }
+
+ /**
+ * Sanitize the artifact.
+ */
+ private function sanitizeArtifact(Closure $outputCallback, string $artifactDir): void
+ {
+ $outputCallback('out', 'Finding Drupal core text files');
+ $sanitizeFinder = $this->localMachineHelper->getFinder()
+ ->files()
+ ->name('*.txt')
+ ->notName('LICENSE.txt')
+ ->in("$artifactDir/docroot/core");
+
+ $outputCallback('out', 'Finding VCS directories');
+ $vcsFinder = $this->localMachineHelper->getFinder()
+ ->ignoreDotFiles(false)
+ ->ignoreVCS(false)
+ ->directories()
+ ->in(["$artifactDir/docroot",
"$artifactDir/vendor",
- ])
- ->name('.git');
- $drushDir = "$artifactDir/drush";
- if (file_exists($drushDir)) {
- $vcsFinder->in($drushDir);
- }
- if ($vcsFinder->hasResults()) {
- $sanitizeFinder->append($vcsFinder);
- }
-
- $outputCallback('out', 'Finding INSTALL database text files');
- $dbInstallFinder = $this->localMachineHelper->getFinder()
- ->files()
- ->in([$artifactDir])
- ->name('/INSTALL\.[a-z]+\.(md|txt)$/');
- if ($dbInstallFinder->hasResults()) {
- $sanitizeFinder->append($dbInstallFinder);
- }
-
- $outputCallback('out', 'Finding other common text files');
- $filenames = [
- 'AUTHORS',
- 'CHANGELOG',
- 'CONDUCT',
- 'CONTRIBUTING',
- 'INSTALL',
- 'MAINTAINERS',
- 'PATCHES',
- 'TESTING',
- 'UPDATE',
- ];
- $textFileFinder = $this->localMachineHelper->getFinder()
- ->files()
- ->in(["$artifactDir/docroot"])
- ->name('/(' . implode('|', $filenames) . ')\.(md|txt)$/');
- if ($textFileFinder->hasResults()) {
- $sanitizeFinder->append($textFileFinder);
- }
-
- $outputCallback('out', "Removing sensitive files from build");
- $this->localMachineHelper->getFilesystem()->remove($sanitizeFinder);
- }
-
- /**
- * Commit the artifact.
- */
- private function commit(Closure $outputCallback, string $artifactDir, string $commitHash): void {
- $outputCallback('out', 'Adding and committing changed files');
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $process = $this->localMachineHelper->execute(['git', 'add', '-A'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Could not add files to artifact via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]);
- }
- foreach (array_merge($this->vendorDirs(), $this->scaffoldFiles($artifactDir)) as $file) {
- $this->logger->debug("Forcibly adding $file");
- $this->localMachineHelper->execute(['git', 'add', '-f', $file], NULL, $artifactDir, FALSE);
- if (!$process->isSuccessful()) {
- // This will fatally error if the file doesn't exist. Suppress error output.
- $this->io->warning("Unable to forcibly add $file to new branch");
- }
- }
- $commitMessage = $this->generateCommitMessage($commitHash);
- $process = $this->localMachineHelper->execute(['git', 'commit', '-m', $commitMessage], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Could not commit via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]);
- }
- }
-
- private function generateCommitMessage(string $commitHash): array|string {
- if ($envVar = getenv('ACLI_PUSH_ARTIFACT_COMMIT_MSG')) {
- return $envVar;
- }
-
- return "Automated commit by Acquia CLI (source commit: $commitHash)";
- }
-
- /**
- * Push the artifact.
- */
- private function pushArtifact(Closure $outputCallback, string $artifactDir, array $vcsUrls, string $destGitBranch): void {
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- foreach ($vcsUrls as $vcsUrl) {
- $outputCallback('out', "Pushing changes to Acquia Git ($vcsUrl)");
- $args = [
+ ])
+ ->name('.git');
+ $drushDir = "$artifactDir/drush";
+ if (file_exists($drushDir)) {
+ $vcsFinder->in($drushDir);
+ }
+ if ($vcsFinder->hasResults()) {
+ $sanitizeFinder->append($vcsFinder);
+ }
+
+ $outputCallback('out', 'Finding INSTALL database text files');
+ $dbInstallFinder = $this->localMachineHelper->getFinder()
+ ->files()
+ ->in([$artifactDir])
+ ->name('/INSTALL\.[a-z]+\.(md|txt)$/');
+ if ($dbInstallFinder->hasResults()) {
+ $sanitizeFinder->append($dbInstallFinder);
+ }
+
+ $outputCallback('out', 'Finding other common text files');
+ $filenames = [
+ 'AUTHORS',
+ 'CHANGELOG',
+ 'CONDUCT',
+ 'CONTRIBUTING',
+ 'INSTALL',
+ 'MAINTAINERS',
+ 'PATCHES',
+ 'TESTING',
+ 'UPDATE',
+ ];
+ $textFileFinder = $this->localMachineHelper->getFinder()
+ ->files()
+ ->in(["$artifactDir/docroot"])
+ ->name('/(' . implode('|', $filenames) . ')\.(md|txt)$/');
+ if ($textFileFinder->hasResults()) {
+ $sanitizeFinder->append($textFileFinder);
+ }
+
+ $outputCallback('out', "Removing sensitive files from build");
+ $this->localMachineHelper->getFilesystem()->remove($sanitizeFinder);
+ }
+
+ /**
+ * Commit the artifact.
+ */
+ private function commit(Closure $outputCallback, string $artifactDir, string $commitHash): void
+ {
+ $outputCallback('out', 'Adding and committing changed files');
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $process = $this->localMachineHelper->execute(['git', 'add', '-A'], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Could not add files to artifact via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]);
+ }
+ foreach (array_merge($this->vendorDirs(), $this->scaffoldFiles($artifactDir)) as $file) {
+ $this->logger->debug("Forcibly adding $file");
+ $this->localMachineHelper->execute(['git', 'add', '-f', $file], null, $artifactDir, false);
+ if (!$process->isSuccessful()) {
+ // This will fatally error if the file doesn't exist. Suppress error output.
+ $this->io->warning("Unable to forcibly add $file to new branch");
+ }
+ }
+ $commitMessage = $this->generateCommitMessage($commitHash);
+ $process = $this->localMachineHelper->execute(['git', 'commit', '-m', $commitMessage], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Could not commit via git: {message}", ['message' => $process->getErrorOutput() . $process->getOutput()]);
+ }
+ }
+
+ private function generateCommitMessage(string $commitHash): array|string
+ {
+ if ($envVar = getenv('ACLI_PUSH_ARTIFACT_COMMIT_MSG')) {
+ return $envVar;
+ }
+
+ return "Automated commit by Acquia CLI (source commit: $commitHash)";
+ }
+
+ /**
+ * Push the artifact.
+ */
+ private function pushArtifact(Closure $outputCallback, string $artifactDir, array $vcsUrls, string $destGitBranch): void
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ foreach ($vcsUrls as $vcsUrl) {
+ $outputCallback('out', "Pushing changes to Acquia Git ($vcsUrl)");
+ $args = [
+ 'git',
+ 'push',
+ $vcsUrl,
+ $destGitBranch,
+ ];
+ $process = $this->localMachineHelper->execute($args, $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException("Unable to push artifact: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]);
+ }
+ }
+ }
+
+ /**
+ * Get a list of Composer vendor directories from the root composer.json.
+ *
+ * @return array|string[]
+ */
+ private function vendorDirs(): array
+ {
+ if (!empty($this->vendorDirs)) {
+ return $this->vendorDirs;
+ }
+
+ $this->vendorDirs = [
+ 'vendor',
+ ];
+ if (file_exists($this->composerJsonPath)) {
+ $composerJson = json_decode($this->localMachineHelper->readFile($this->composerJsonPath), true, 512, JSON_THROW_ON_ERROR);
+
+ foreach ($composerJson['extra']['installer-paths'] as $path => $type) {
+ $this->vendorDirs[] = str_replace('/{$name}', '', $path);
+ }
+ return $this->vendorDirs;
+ }
+ return [];
+ }
+
+ /**
+ * Get a list of scaffold files from Drupal core's composer.json.
+ *
+ * @return array
+ */
+ private function scaffoldFiles(string $artifactDir): array
+ {
+ if (!empty($this->scaffoldFiles)) {
+ return $this->scaffoldFiles;
+ }
+
+ $this->scaffoldFiles = [];
+ $composerJson = json_decode($this->localMachineHelper->readFile(Path::join($artifactDir, 'docroot', 'core', 'composer.json')), true, 512, JSON_THROW_ON_ERROR);
+ foreach ($composerJson['extra']['drupal-scaffold']['file-mapping'] as $file => $assetPath) {
+ if (str_starts_with($file, '[web-root]')) {
+ $this->scaffoldFiles[] = str_replace('[web-root]', 'docroot', $file);
+ }
+ }
+ $this->scaffoldFiles[] = 'docroot/autoload.php';
+
+ return $this->scaffoldFiles;
+ }
+
+ private function validateSourceCode(): void
+ {
+ $requiredPaths = [
+ $this->composerJsonPath,
+ $this->docrootPath,
+ ];
+ foreach ($requiredPaths as $requiredPath) {
+ if (!file_exists($requiredPath)) {
+ throw new AcquiaCliException("Your current directory does not look like a valid Drupal application. $requiredPath is missing.");
+ }
+ }
+ }
+
+ private function determineSourceGitRef(): string
+ {
+ if ($this->input->getOption('source-git-tag')) {
+ return $this->input->getOption('source-git-tag');
+ }
+ if ($envVar = getenv('ACLI_PUSH_ARTIFACT_SOURCE_GIT_TAG')) {
+ return $envVar;
+ }
+ if ($this->input->getOption('destination-git-tag')) {
+ throw new AcquiaCliException('You must also set the --source-git-tag option when setting the --destination-git-tag option.');
+ }
+
+ // Assume the source and destination branches are the same.
+ return $this->destinationGitRef;
+ }
+
+ private function determineDestinationGitRef(): string
+ {
+ if ($this->input->getOption('destination-git-tag')) {
+ $this->destinationGitRef = $this->input->getOption('destination-git-tag');
+ return $this->destinationGitRef;
+ }
+ if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_TAG')) {
+ $this->destinationGitRef = $envVar;
+ return $this->destinationGitRef;
+ }
+ if ($this->input->getOption('destination-git-branch')) {
+ $this->destinationGitRef = $this->input->getOption('destination-git-branch');
+ return $this->destinationGitRef;
+ }
+ if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_BRANCH')) {
+ $this->destinationGitRef = $envVar;
+ return $this->destinationGitRef;
+ }
+
+ $environment = $this->determineEnvironment($this->input, $this->output);
+ if (str_starts_with($environment->vcs->path, 'tags')) {
+ throw new AcquiaCliException("You cannot push to an environment that has a git tag deployed to it. Environment $environment->name has {$environment->vcs->path} deployed. Select a different environment.");
+ }
+
+ $this->destinationGitRef = $environment->vcs->path;
+
+ return $this->destinationGitRef;
+ }
+
+ private function createTag(mixed $tagName, Closure $outputCallback, string $artifactDir): void
+ {
+ $this->localMachineHelper->checkRequiredBinariesExist(['git']);
+ $process = $this->localMachineHelper->execute([
'git',
- 'push',
- $vcsUrl,
- $destGitBranch,
- ];
- $process = $this->localMachineHelper->execute($args, $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException("Unable to push artifact: {message}", ['message' => $process->getOutput() . $process->getErrorOutput()]);
- }
- }
- }
-
- /**
- * Get a list of Composer vendor directories from the root composer.json.
- *
- * @return array|string[]
- */
- private function vendorDirs(): array {
- if (!empty($this->vendorDirs)) {
- return $this->vendorDirs;
+ 'tag',
+ $tagName,
+ ], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Failed to create Git tag: {message}', ['message' => $process->getErrorOutput()]);
+ }
}
-
- $this->vendorDirs = [
- 'vendor',
- ];
- if (file_exists($this->composerJsonPath)) {
- $composerJson = json_decode($this->localMachineHelper->readFile($this->composerJsonPath), TRUE, 512, JSON_THROW_ON_ERROR);
-
- foreach ($composerJson['extra']['installer-paths'] as $path => $type) {
- $this->vendorDirs[] = str_replace('/{$name}', '', $path);
- }
- return $this->vendorDirs;
- }
- return [];
- }
-
- /**
- * Get a list of scaffold files from Drupal core's composer.json.
- *
- * @return array
- */
- private function scaffoldFiles(string $artifactDir): array {
- if (!empty($this->scaffoldFiles)) {
- return $this->scaffoldFiles;
- }
-
- $this->scaffoldFiles = [];
- $composerJson = json_decode($this->localMachineHelper->readFile(Path::join($artifactDir, 'docroot', 'core', 'composer.json')), TRUE, 512, JSON_THROW_ON_ERROR);
- foreach ($composerJson['extra']['drupal-scaffold']['file-mapping'] as $file => $assetPath) {
- if (str_starts_with($file, '[web-root]')) {
- $this->scaffoldFiles[] = str_replace('[web-root]', 'docroot', $file);
- }
- }
- $this->scaffoldFiles[] = 'docroot/autoload.php';
-
- return $this->scaffoldFiles;
- }
-
- private function validateSourceCode(): void {
- $requiredPaths = [
- $this->composerJsonPath,
- $this->docrootPath,
- ];
- foreach ($requiredPaths as $requiredPath) {
- if (!file_exists($requiredPath)) {
- throw new AcquiaCliException("Your current directory does not look like a valid Drupal application. $requiredPath is missing.");
- }
- }
- }
-
- private function determineSourceGitRef(): string {
- if ($this->input->getOption('source-git-tag')) {
- return $this->input->getOption('source-git-tag');
- }
- if ($envVar = getenv('ACLI_PUSH_ARTIFACT_SOURCE_GIT_TAG')) {
- return $envVar;
- }
- if ($this->input->getOption('destination-git-tag')) {
- throw new AcquiaCliException('You must also set the --source-git-tag option when setting the --destination-git-tag option.');
- }
-
- // Assume the source and destination branches are the same.
- return $this->destinationGitRef;
- }
-
- private function determineDestinationGitRef(): string {
- if ($this->input->getOption('destination-git-tag')) {
- $this->destinationGitRef = $this->input->getOption('destination-git-tag');
- return $this->destinationGitRef;
- }
- if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_TAG')) {
- $this->destinationGitRef = $envVar;
- return $this->destinationGitRef;
- }
- if ($this->input->getOption('destination-git-branch')) {
- $this->destinationGitRef = $this->input->getOption('destination-git-branch');
- return $this->destinationGitRef;
- }
- if ($envVar = getenv('ACLI_PUSH_ARTIFACT_DESTINATION_GIT_BRANCH')) {
- $this->destinationGitRef = $envVar;
- return $this->destinationGitRef;
- }
-
- $environment = $this->determineEnvironment($this->input, $this->output);
- if (str_starts_with($environment->vcs->path, 'tags')) {
- throw new AcquiaCliException("You cannot push to an environment that has a git tag deployed to it. Environment $environment->name has {$environment->vcs->path} deployed. Select a different environment.");
- }
-
- $this->destinationGitRef = $environment->vcs->path;
-
- return $this->destinationGitRef;
- }
-
- private function createTag(mixed $tagName, Closure $outputCallback, string $artifactDir): void {
- $this->localMachineHelper->checkRequiredBinariesExist(['git']);
- $process = $this->localMachineHelper->execute([
- 'git',
- 'tag',
- $tagName,
- ], $outputCallback, $artifactDir, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Failed to create Git tag: {message}', ['message' => $process->getErrorOutput()]);
- }
- }
-
}
diff --git a/src/Command/Push/PushCodeCommand.php b/src/Command/Push/PushCodeCommand.php
index 9f96c0d10..f05b0577e 100644
--- a/src/Command/Push/PushCodeCommand.php
+++ b/src/Command/Push/PushCodeCommand.php
@@ -1,6 +1,6 @@
setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !self::isLandoEnv());
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $output->writeln("Use git> to push code changes upstream.");
-
- return Command::SUCCESS;
- }
-
+final class PushCodeCommand extends PushCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !self::isLandoEnv());
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $output->writeln("Use git> to push code changes upstream.");
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Push/PushCommandBase.php b/src/Command/Push/PushCommandBase.php
index 2b6c29bd0..94dbb73d7 100644
--- a/src/Command/Push/PushCommandBase.php
+++ b/src/Command/Push/PushCommandBase.php
@@ -1,14 +1,13 @@
acceptEnvironmentId()
- ->acceptSite();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $destinationEnvironment = $this->determineEnvironment($input, $output);
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $databases = $this->determineCloudDatabases($acquiaCloudClient, $destinationEnvironment, $input->getArgument('site'));
- // We only support pushing a single database.
- $database = $databases[0];
- $answer = $this->io->confirm("Overwrite the $database->name> database on {$destinationEnvironment->name}> with a copy of the database from the current machine?");
- if (!$answer) {
- return Command::SUCCESS;
+final class PushDatabaseCommand extends PushCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId()
+ ->acceptSite();
}
- $this->checklist = new Checklist($output);
- $outputCallback = $this->getOutputCallback($output, $this->checklist);
-
- $this->checklist->addItem('Creating local database dump');
- $localDumpFilepath = $this->createMySqlDumpOnLocal($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $outputCallback);
- $this->checklist->completePreviousItem();
-
- $this->checklist->addItem('Uploading database dump to remote machine');
- $remoteDumpFilepath = $this->uploadDatabaseDump($destinationEnvironment, $localDumpFilepath, $outputCallback);
- $this->checklist->completePreviousItem();
-
- $this->checklist->addItem('Importing database dump into MySQL on remote machine');
- $this->importDatabaseDumpOnRemote($destinationEnvironment, $remoteDumpFilepath, $database);
- $this->checklist->completePreviousItem();
-
- return Command::SUCCESS;
- }
-
- private function uploadDatabaseDump(
- EnvironmentResponse $environment,
- string $localFilepath,
- callable $outputCallback
- ): string {
- $envAlias = self::getEnvironmentAlias($environment);
- $remoteFilepath = "/mnt/tmp/$envAlias/" . basename($localFilepath);
- $this->logger->debug("Uploading database dump to $remoteFilepath on remote machine");
- $this->localMachineHelper->checkRequiredBinariesExist(['rsync']);
- $command = [
- 'rsync',
- '-tDvPhe',
- 'ssh -o StrictHostKeyChecking=no',
- $localFilepath,
- $environment->sshUrl . ':' . $remoteFilepath,
- ];
- $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Could not upload local database dump: {message}',
- ['message' => $process->getOutput()]);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $destinationEnvironment = $this->determineEnvironment($input, $output);
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $databases = $this->determineCloudDatabases($acquiaCloudClient, $destinationEnvironment, $input->getArgument('site'));
+ // We only support pushing a single database.
+ $database = $databases[0];
+ $answer = $this->io->confirm("Overwrite the $database->name> database on {$destinationEnvironment->name}> with a copy of the database from the current machine?");
+ if (!$answer) {
+ return Command::SUCCESS;
+ }
+
+ $this->checklist = new Checklist($output);
+ $outputCallback = $this->getOutputCallback($output, $this->checklist);
+
+ $this->checklist->addItem('Creating local database dump');
+ $localDumpFilepath = $this->createMySqlDumpOnLocal($this->getLocalDbHost(), $this->getLocalDbUser(), $this->getLocalDbName(), $this->getLocalDbPassword(), $outputCallback);
+ $this->checklist->completePreviousItem();
+
+ $this->checklist->addItem('Uploading database dump to remote machine');
+ $remoteDumpFilepath = $this->uploadDatabaseDump($destinationEnvironment, $localDumpFilepath, $outputCallback);
+ $this->checklist->completePreviousItem();
+
+ $this->checklist->addItem('Importing database dump into MySQL on remote machine');
+ $this->importDatabaseDumpOnRemote($destinationEnvironment, $remoteDumpFilepath, $database);
+ $this->checklist->completePreviousItem();
+
+ return Command::SUCCESS;
}
- return $remoteFilepath;
- }
-
- private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void {
- $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine");
- $command = "pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD={$database->password} mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user={$database->user_name} {$this->getNameFromDatabaseResponse($database)}";
- $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]);
+ private function uploadDatabaseDump(
+ EnvironmentResponse $environment,
+ string $localFilepath,
+ callable $outputCallback
+ ): string {
+ $envAlias = self::getEnvironmentAlias($environment);
+ $remoteFilepath = "/mnt/tmp/$envAlias/" . basename($localFilepath);
+ $this->logger->debug("Uploading database dump to $remoteFilepath on remote machine");
+ $this->localMachineHelper->checkRequiredBinariesExist(['rsync']);
+ $command = [
+ 'rsync',
+ '-tDvPhe',
+ 'ssh -o StrictHostKeyChecking=no',
+ $localFilepath,
+ $environment->sshUrl . ':' . $remoteFilepath,
+ ];
+ $process = $this->localMachineHelper->execute($command, $outputCallback, null, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException(
+ 'Could not upload local database dump: {message}',
+ ['message' => $process->getOutput()]
+ );
+ }
+
+ return $remoteFilepath;
}
- }
- private function getNameFromDatabaseResponse(DatabaseResponse $database): string {
- $dbUrlParts = explode('/', $database->url);
- return end($dbUrlParts);
- }
+ private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void
+ {
+ $this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine");
+ $command = "pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD={$database->password} mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user={$database->user_name} {$this->getNameFromDatabaseResponse($database)}";
+ $process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]);
+ }
+ }
+ private function getNameFromDatabaseResponse(DatabaseResponse $database): string
+ {
+ $dbUrlParts = explode('/', $database->url);
+ return end($dbUrlParts);
+ }
}
diff --git a/src/Command/Push/PushFilesCommand.php b/src/Command/Push/PushFilesCommand.php
index 08fe07661..1414bbaf6 100644
--- a/src/Command/Push/PushFilesCommand.php
+++ b/src/Command/Push/PushFilesCommand.php
@@ -1,6 +1,6 @@
acceptEnvironmentId()
- ->acceptSite();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $this->setDirAndRequireProjectCwd($input);
- $destinationEnvironment = $this->determineEnvironment($input, $output);
- $chosenSite = $input->getArgument('site');
- if (!$chosenSite) {
- $chosenSite = $this->promptChooseDrupalSite($destinationEnvironment);
+final class PushFilesCommand extends PushCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->acceptEnvironmentId()
+ ->acceptSite();
}
- $answer = $this->io->confirm("Overwrite the public files directory on $destinationEnvironment->name> with a copy of the files from the current machine?");
- if (!$answer) {
- return Command::SUCCESS;
- }
-
- $this->checklist = new Checklist($output);
- $this->checklist->addItem('Pushing public files directory to remote machine');
- $this->rsyncFilesToCloud($destinationEnvironment, $this->getOutputCallback($output, $this->checklist), $chosenSite);
- $this->checklist->completePreviousItem();
-
- return Command::SUCCESS;
- }
- private function rsyncFilesToCloud(EnvironmentResponse $chosenEnvironment, callable $outputCallback = NULL, string $site = NULL): void {
- $sourceDir = $this->getLocalFilesDir($site);
- $destinationDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ $destinationEnvironment = $this->determineEnvironment($input, $output);
+ $chosenSite = $input->getArgument('site');
+ if (!$chosenSite) {
+ $chosenSite = $this->promptChooseDrupalSite($destinationEnvironment);
+ }
+ $answer = $this->io->confirm("Overwrite the public files directory on $destinationEnvironment->name> with a copy of the files from the current machine?");
+ if (!$answer) {
+ return Command::SUCCESS;
+ }
+
+ $this->checklist = new Checklist($output);
+ $this->checklist->addItem('Pushing public files directory to remote machine');
+ $this->rsyncFilesToCloud($destinationEnvironment, $this->getOutputCallback($output, $this->checklist), $chosenSite);
+ $this->checklist->completePreviousItem();
+
+ return Command::SUCCESS;
+ }
- $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback);
- }
+ private function rsyncFilesToCloud(EnvironmentResponse $chosenEnvironment, callable $outputCallback = null, string $site = null): void
+ {
+ $sourceDir = $this->getLocalFilesDir($site);
+ $destinationDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site);
+ $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback);
+ }
}
diff --git a/src/Command/Remote/AliasListCommand.php b/src/Command/Remote/AliasListCommand.php
index 21c239f2a..bb769778b 100644
--- a/src/Command/Remote/AliasListCommand.php
+++ b/src/Command/Remote/AliasListCommand.php
@@ -1,6 +1,6 @@
acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $applicationsResource = new Applications($acquiaCloudClient);
- $cloudApplicationUuid = $this->determineCloudApplication();
- $customerApplication = $applicationsResource->get($cloudApplicationUuid);
- $environmentsResource = new Environments($acquiaCloudClient);
-
- $table = new Table($this->output);
- $table->setHeaders(['Application', 'Environment Alias', 'Environment UUID']);
-
- $siteId = $customerApplication->hosting->id;
- $parts = explode(':', $siteId);
- $sitePrefix = $parts[1];
- $environments = $environmentsResource->getAll($customerApplication->uuid);
- foreach ($environments as $environment) {
- $alias = $sitePrefix . '.' . $environment->name;
- $table->addRow([$customerApplication->name, $alias, $environment->uuid]);
+final class AliasListCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this->acceptApplicationUuid();
}
- $table->render();
-
- return Command::SUCCESS;
- }
-
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $applicationsResource = new Applications($acquiaCloudClient);
+ $cloudApplicationUuid = $this->determineCloudApplication();
+ $customerApplication = $applicationsResource->get($cloudApplicationUuid);
+ $environmentsResource = new Environments($acquiaCloudClient);
+
+ $table = new Table($this->output);
+ $table->setHeaders(['Application', 'Environment Alias', 'Environment UUID']);
+
+ $siteId = $customerApplication->hosting->id;
+ $parts = explode(':', $siteId);
+ $sitePrefix = $parts[1];
+ $environments = $environmentsResource->getAll($customerApplication->uuid);
+ foreach ($environments as $environment) {
+ $alias = $sitePrefix . '.' . $environment->name;
+ $table->addRow([$customerApplication->name, $alias, $environment->uuid]);
+ }
+
+ $table->render();
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Remote/AliasesDownloadCommand.php b/src/Command/Remote/AliasesDownloadCommand.php
index 7579e8895..89f7a0978 100644
--- a/src/Command/Remote/AliasesDownloadCommand.php
+++ b/src/Command/Remote/AliasesDownloadCommand.php
@@ -1,6 +1,6 @@
addOption('destination-dir', NULL, InputOption::VALUE_REQUIRED, 'The directory to which aliases will be downloaded')
- ->addOption('all', NULL, InputOption::VALUE_NONE, 'Download the aliases for all applications that you have access to, not just the current one.');
- $this->acceptApplicationUuid();
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $aliasVersion = $this->promptChooseDrushAliasVersion();
- $drushArchiveTempFilepath = $this->getDrushArchiveTempFilepath();
- $drushAliasesDir = $this->getDrushAliasesDir($aliasVersion);
- $this->localMachineHelper->getFilesystem()->mkdir($drushAliasesDir);
- $this->localMachineHelper->getFilesystem()->chmod($drushAliasesDir, 0700);
-
- if ($aliasVersion === '9') {
- $this->downloadDrush9Aliases($input, $aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir);
+final class AliasesDownloadCommand extends SshBaseCommand
+{
+ private string $drushArchiveFilepath;
+
+ protected function configure(): void
+ {
+ $this
+ ->addOption('destination-dir', null, InputOption::VALUE_REQUIRED, 'The directory to which aliases will be downloaded')
+ ->addOption('all', null, InputOption::VALUE_NONE, 'Download the aliases for all applications that you have access to, not just the current one.');
+ $this->acceptApplicationUuid();
}
- else {
- $this->downloadDrush8Aliases($aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir);
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $aliasVersion = $this->promptChooseDrushAliasVersion();
+ $drushArchiveTempFilepath = $this->getDrushArchiveTempFilepath();
+ $drushAliasesDir = $this->getDrushAliasesDir($aliasVersion);
+ $this->localMachineHelper->getFilesystem()->mkdir($drushAliasesDir);
+ $this->localMachineHelper->getFilesystem()->chmod($drushAliasesDir, 0700);
+
+ if ($aliasVersion === '9') {
+ $this->downloadDrush9Aliases($input, $aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir);
+ } else {
+ $this->downloadDrush8Aliases($aliasVersion, $drushArchiveTempFilepath, $drushAliasesDir);
+ }
+
+ $this->output->writeln(sprintf(
+ 'Cloud Platform Drush aliases installed into %s>',
+ $drushAliasesDir
+ ));
+ unlink($drushArchiveTempFilepath);
+
+ return Command::SUCCESS;
}
- $this->output->writeln(sprintf(
- 'Cloud Platform Drush aliases installed into %s>',
- $drushAliasesDir
- ));
- unlink($drushArchiveTempFilepath);
-
- return Command::SUCCESS;
- }
-
- /**
- * Prompts the user for their preferred Drush alias version.
- */
- protected function promptChooseDrushAliasVersion(): string {
- $this->io->writeln('Drush changed how aliases are defined in Drush 9. Drush 8 aliases are PHP-based and stored in your home directory, while Drush 9+ aliases are YAML-based and stored with your project.');
- $question = 'Choose your preferred alias compatibility:';
- $choices = [
- '8' => 'Drush 8 / Drupal 7 (PHP)',
- '9' => 'Drush 9+ / Drupal 8+ (YAML)',
- ];
- return (string) array_search($this->io->choice($question, $choices, '9'), $choices, TRUE);
- }
-
- public function getDrushArchiveTempFilepath(): string {
- if (!isset($this->drushArchiveFilepath)) {
- $this->drushArchiveFilepath = tempnam(sys_get_temp_dir(),
- 'AcquiaDrushAliases') . '.tar.gz';
+ /**
+ * Prompts the user for their preferred Drush alias version.
+ */
+ protected function promptChooseDrushAliasVersion(): string
+ {
+ $this->io->writeln('Drush changed how aliases are defined in Drush 9. Drush 8 aliases are PHP-based and stored in your home directory, while Drush 9+ aliases are YAML-based and stored with your project.');
+ $question = 'Choose your preferred alias compatibility:';
+ $choices = [
+ '8' => 'Drush 8 / Drupal 7 (PHP)',
+ '9' => 'Drush 9+ / Drupal 8+ (YAML)',
+ ];
+ return (string) array_search($this->io->choice($question, $choices, '9'), $choices, true);
}
- return $this->drushArchiveFilepath;
- }
+ public function getDrushArchiveTempFilepath(): string
+ {
+ if (!isset($this->drushArchiveFilepath)) {
+ $this->drushArchiveFilepath = tempnam(
+ sys_get_temp_dir(),
+ 'AcquiaDrushAliases'
+ ) . '.tar.gz';
+ }
- protected function getDrushAliasesDir(string $version): string {
- if ($this->input->getOption('destination-dir')) {
- return $this->input->getOption('destination-dir');
- }
- return match ($version) {
- '8' => Path::join($this->localMachineHelper::getHomeDir(), '.drush'),
- '9' => Path::join($this->getProjectDir(), 'drush'),
- default => throw new AcquiaCliException("Unknown Drush version"),
- };
- }
-
- protected function getAliasesFromCloud(Client $acquiaCloudClient, string $aliasVersion): StreamInterface {
- $acquiaCloudClient->addQuery('version', $aliasVersion);
- return (new Account($acquiaCloudClient))->getDrushAliases();
- }
-
- protected function getSitePrefix(bool $singleApplication): string {
- $sitePrefix = '';
- if ($singleApplication) {
- $cloudApplicationUuid = $this->determineCloudApplication();
- $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
- $parts = explode(':', $cloudApplication->hosting->id);
- $sitePrefix = $parts[1];
- }
- return $sitePrefix;
- }
-
- protected function downloadArchive(string $aliasVersion, string $drushArchiveTempFilepath, string $baseDir): PharData {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $aliases = $this->getAliasesFromCloud($acquiaCloudClient, $aliasVersion);
- $this->localMachineHelper->writeFile($drushArchiveTempFilepath, $aliases);
- return new PharData($drushArchiveTempFilepath . '/' . $baseDir);
- }
-
- protected function downloadDrush9Aliases(InputInterface $input, string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void {
- $this->setDirAndRequireProjectCwd($input);
- $all = $input->getOption('all');
- $applicationUuidArgument = $input->getArgument('applicationUuid');
- $singleApplication = !$all || $applicationUuidArgument;
- $sitePrefix = $this->getSitePrefix($singleApplication);
- $baseDir = 'sites';
- $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir);
- if ($singleApplication) {
- $drushFiles = $this->getSingleAliasForSite($archive, $sitePrefix, $baseDir);
+ return $this->drushArchiveFilepath;
}
- else {
- $drushFiles = [];
- foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
- $drushFiles[] = $baseDir . '/' . $file->getFileName();
- }
+
+ protected function getDrushAliasesDir(string $version): string
+ {
+ if ($this->input->getOption('destination-dir')) {
+ return $this->input->getOption('destination-dir');
+ }
+ return match ($version) {
+ '8' => Path::join($this->localMachineHelper::getHomeDir(), '.drush'),
+ '9' => Path::join($this->getProjectDir(), 'drush'),
+ default => throw new AcquiaCliException("Unknown Drush version"),
+ };
}
- try {
- // Throws warnings on permissions errors.
- @$archive->extractTo($drushAliasesDir, $drushFiles, TRUE);
+
+ protected function getAliasesFromCloud(Client $acquiaCloudClient, string $aliasVersion): StreamInterface
+ {
+ $acquiaCloudClient->addQuery('version', $aliasVersion);
+ return (new Account($acquiaCloudClient))->getDrushAliases();
}
- catch (\Exception) {
- throw new AcquiaCliException('Could not extract aliases to {destination}', ['destination' => $drushAliasesDir]);
+
+ protected function getSitePrefix(bool $singleApplication): string
+ {
+ $sitePrefix = '';
+ if ($singleApplication) {
+ $cloudApplicationUuid = $this->determineCloudApplication();
+ $cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
+ $parts = explode(':', $cloudApplication->hosting->id);
+ $sitePrefix = $parts[1];
+ }
+ return $sitePrefix;
}
- }
-
- protected function downloadDrush8Aliases(string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void {
- $baseDir = '.drush';
- $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir);
- $drushFiles = [];
- foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
- $drushFiles[] = $baseDir . '/' . $file->getFileName();
+
+ protected function downloadArchive(string $aliasVersion, string $drushArchiveTempFilepath, string $baseDir): PharData
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $aliases = $this->getAliasesFromCloud($acquiaCloudClient, $aliasVersion);
+ $this->localMachineHelper->writeFile($drushArchiveTempFilepath, $aliases);
+ return new PharData($drushArchiveTempFilepath . '/' . $baseDir);
}
- $archive->extractTo($drushAliasesDir, $drushFiles, TRUE);
- }
-
- /**
- * @return array
- */
- protected function getSingleAliasForSite(PharData $archive, string $sitePrefix, string $baseDir): array {
- $drushFiles = [];
- foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
- // Just get the single alias for this single application.
- if ($file->getFileName() === $sitePrefix . '.site.yml') {
- $drushFiles[] = $baseDir . '/' . $file->getFileName();
- break;
- }
+
+ protected function downloadDrush9Aliases(InputInterface $input, string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void
+ {
+ $this->setDirAndRequireProjectCwd($input);
+ $all = $input->getOption('all');
+ $applicationUuidArgument = $input->getArgument('applicationUuid');
+ $singleApplication = !$all || $applicationUuidArgument;
+ $sitePrefix = $this->getSitePrefix($singleApplication);
+ $baseDir = 'sites';
+ $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir);
+ if ($singleApplication) {
+ $drushFiles = $this->getSingleAliasForSite($archive, $sitePrefix, $baseDir);
+ } else {
+ $drushFiles = [];
+ foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
+ $drushFiles[] = $baseDir . '/' . $file->getFileName();
+ }
+ }
+ try {
+ // Throws warnings on permissions errors.
+ @$archive->extractTo($drushAliasesDir, $drushFiles, true);
+ } catch (\Exception) {
+ throw new AcquiaCliException('Could not extract aliases to {destination}', ['destination' => $drushAliasesDir]);
+ }
}
- if (empty($drushFiles)) {
- throw new AcquiaCliException("Could not locate any aliases matching the current site ($sitePrefix)");
+
+ protected function downloadDrush8Aliases(string $aliasVersion, string $drushArchiveTempFilepath, string $drushAliasesDir): void
+ {
+ $baseDir = '.drush';
+ $archive = $this->downloadArchive($aliasVersion, $drushArchiveTempFilepath, $baseDir);
+ $drushFiles = [];
+ foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
+ $drushFiles[] = $baseDir . '/' . $file->getFileName();
+ }
+ $archive->extractTo($drushAliasesDir, $drushFiles, true);
}
- return $drushFiles;
- }
+ /**
+ * @return array
+ */
+ protected function getSingleAliasForSite(PharData $archive, string $sitePrefix, string $baseDir): array
+ {
+ $drushFiles = [];
+ foreach (new RecursiveIteratorIterator($archive, RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
+ // Just get the single alias for this single application.
+ if ($file->getFileName() === $sitePrefix . '.site.yml') {
+ $drushFiles[] = $baseDir . '/' . $file->getFileName();
+ break;
+ }
+ }
+ if (empty($drushFiles)) {
+ throw new AcquiaCliException("Could not locate any aliases matching the current site ($sitePrefix)");
+ }
+ return $drushFiles;
+ }
}
diff --git a/src/Command/Remote/DrushCommand.php b/src/Command/Remote/DrushCommand.php
index df1372fa1..dad6feb32 100644
--- a/src/Command/Remote/DrushCommand.php
+++ b/src/Command/Remote/DrushCommand.php
@@ -1,6 +1,6 @@
setHelp('Pay close attention to the argument syntax! Note the usage of --> to separate the drush command arguments and options.>')
- ->acceptEnvironmentId()
- ->addArgument('drush_command', InputArgument::IS_ARRAY, 'Drush command')
- ->addUsage('. -- ')
- ->addUsage('myapp.dev -- uli 1')
- ->addUsage('myapp.dev -- status --fields=db-status');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): ?int {
- $environment = $this->determineEnvironment($input, $output);
- $alias = self::getEnvironmentAlias($environment);
- $acliArguments = $input->getArguments();
- $drushArguments = (array) $acliArguments['drush_command'];
- // When available, provide the default domain to drush.
- if (!empty($environment->default_domain)) {
- // Insert at the beginning so a user-supplied --uri arg will override.
- array_unshift($drushArguments, "--uri=http://$environment->default_domain");
+final class DrushCommand extends SshBaseCommand
+{
+ protected function configure(): void
+ {
+ $this
+ ->setHelp('Pay close attention to the argument syntax! Note the usage of --> to separate the drush command arguments and options.>')
+ ->acceptEnvironmentId()
+ ->addArgument('drush_command', InputArgument::IS_ARRAY, 'Drush command')
+ ->addUsage('. -- ')
+ ->addUsage('myapp.dev -- uli 1')
+ ->addUsage('myapp.dev -- status --fields=db-status');
}
- $drushCommandArguments = [
- "cd /var/www/html/$alias/docroot; ",
- 'drush',
- implode(' ', $drushArguments),
- ];
-
- return $this->sshHelper->executeCommand($environment->sshUrl, $drushCommandArguments)->getExitCode();
- }
+ protected function execute(InputInterface $input, OutputInterface $output): ?int
+ {
+ $environment = $this->determineEnvironment($input, $output);
+ $alias = self::getEnvironmentAlias($environment);
+ $acliArguments = $input->getArguments();
+ $drushArguments = (array) $acliArguments['drush_command'];
+ // When available, provide the default domain to drush.
+ if (!empty($environment->default_domain)) {
+ // Insert at the beginning so a user-supplied --uri arg will override.
+ array_unshift($drushArguments, "--uri=http://$environment->default_domain");
+ }
+ $drushCommandArguments = [
+ "cd /var/www/html/$alias/docroot; ",
+ 'drush',
+ implode(' ', $drushArguments),
+ ];
+
+ return $this->sshHelper->executeCommand($environment->sshUrl, $drushCommandArguments)->getExitCode();
+ }
}
diff --git a/src/Command/Remote/SshBaseCommand.php b/src/Command/Remote/SshBaseCommand.php
index 08cb35dcc..c3ca29905 100644
--- a/src/Command/Remote/SshBaseCommand.php
+++ b/src/Command/Remote/SshBaseCommand.php
@@ -1,6 +1,6 @@
addArgument('alias', InputArgument::REQUIRED, 'Alias for application & environment in the format `app-name.env`')
- ->addArgument('ssh_command', InputArgument::IS_ARRAY, 'Command to run via SSH (if not provided, opens a shell in the site directory)')
- ->addUsage("myapp.dev # open a shell in the myapp.dev environment")
- ->addUsage("myapp.dev -- ls -al # list files in the myapp.dev environment and return");
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): ?int {
- $alias = $input->getArgument('alias');
- $alias = $this->normalizeAlias($alias);
- $alias = self::validateEnvironmentAlias($alias);
- $environment = $this->getEnvironmentFromAliasArg($alias);
- if (!isset($environment->sshUrl)) {
- throw new AcquiaCliException('Cannot determine environment SSH URL. Check that you have SSH permissions on this environment.');
- }
- $sshCommand = [
- 'cd /var/www/html/' . $alias,
- ];
- $arguments = $input->getArguments();
- if (empty($arguments['ssh_command'])) {
- $sshCommand[] = 'exec $SHELL -l';
+final class SshCommand extends SshBaseCommand
+{
+ protected function configure(): void
+ {
+ $this
+ ->addArgument('alias', InputArgument::REQUIRED, 'Alias for application & environment in the format `app-name.env`')
+ ->addArgument('ssh_command', InputArgument::IS_ARRAY, 'Command to run via SSH (if not provided, opens a shell in the site directory)')
+ ->addUsage("myapp.dev # open a shell in the myapp.dev environment")
+ ->addUsage("myapp.dev -- ls -al # list files in the myapp.dev environment and return");
}
- else {
- $sshCommand[] = implode(' ', $arguments['ssh_command']);
- }
- $sshCommand = (array) implode('; ', $sshCommand);
- return $this->sshHelper->executeCommand($environment->sshUrl, $sshCommand)->getExitCode();
- }
+ protected function execute(InputInterface $input, OutputInterface $output): ?int
+ {
+ $alias = $input->getArgument('alias');
+ $alias = $this->normalizeAlias($alias);
+ $alias = self::validateEnvironmentAlias($alias);
+ $environment = $this->getEnvironmentFromAliasArg($alias);
+ if (!isset($environment->sshUrl)) {
+ throw new AcquiaCliException('Cannot determine environment SSH URL. Check that you have SSH permissions on this environment.');
+ }
+ $sshCommand = [
+ 'cd /var/www/html/' . $alias,
+ ];
+ $arguments = $input->getArguments();
+ if (empty($arguments['ssh_command'])) {
+ $sshCommand[] = 'exec $SHELL -l';
+ } else {
+ $sshCommand[] = implode(' ', $arguments['ssh_command']);
+ }
+ $sshCommand = (array) implode('; ', $sshCommand);
+ return $this->sshHelper->executeCommand($environment->sshUrl, $sshCommand)->getExitCode();
+ }
}
diff --git a/src/Command/Self/ClearCacheCommand.php b/src/Command/Self/ClearCacheCommand.php
index 597134964..f68408ef2 100644
--- a/src/Command/Self/ClearCacheCommand.php
+++ b/src/Command/Self/ClearCacheCommand.php
@@ -1,6 +1,6 @@
writeln('Acquia CLI caches were cleared.');
-
- return Command::SUCCESS;
- }
-
- /**
- * Clear caches.
- */
- public static function clearCaches(): void {
- $cache = self::getAliasCache();
- $cache->clear();
- $systemCacheDir = Path::join(sys_get_temp_dir(), 'symphony-cache');
- $fs = new Filesystem();
- $fs->remove([$systemCacheDir]);
- }
-
+final class ClearCacheCommand extends CommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ self::clearCaches();
+ $output->writeln('Acquia CLI caches were cleared.');
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * Clear caches.
+ */
+ public static function clearCaches(): void
+ {
+ $cache = self::getAliasCache();
+ $cache->clear();
+ $systemCacheDir = Path::join(sys_get_temp_dir(), 'symphony-cache');
+ $fs = new Filesystem();
+ $fs->remove([$systemCacheDir]);
+ }
}
diff --git a/src/Command/Self/ListCommand.php b/src/Command/Self/ListCommand.php
index 836d41058..224302db6 100644
--- a/src/Command/Self/ListCommand.php
+++ b/src/Command/Self/ListCommand.php
@@ -1,6 +1,6 @@
setName('list')
- ->setDefinition([
- new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', NULL, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
- new InputOption('raw', NULL, InputOption::VALUE_NONE, 'To output raw command list'),
- new InputOption('format', NULL, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()),
- new InputOption('short', NULL, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
- ])
- ->setDescription('List commands')
- ->setHelp(<<<'EOF'
+#[AsCommand(name: 'list', description: null, aliases: ['self:list'])]
+final class ListCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->setName('list')
+ ->setDefinition([
+ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()),
+ new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
+ ])
+ ->setDescription('List commands')
+ ->setHelp(<<<'EOF'
The %command.name% command lists all commands:
%command.full_name%
@@ -46,33 +47,33 @@ protected function configure(): void {
%command.full_name% --raw
EOF
- );
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- foreach (['api', 'acsf'] as $prefix) {
- if ($input->getArgument('namespace') !== $prefix) {
- $allCommands = $this->getApplication()->all();
- foreach ($allCommands as $command) {
- if (
- !is_a($command, ApiListCommandBase::class)
- && !is_a($command, AcsfListCommandBase::class)
- && str_starts_with($command->getName(), $prefix . ':')
- ) {
- $command->setHidden();
- }
- }
- }
+ );
}
- $helper = new DescriptorHelper();
- $helper->describe($output, $this->getApplication(), [
- 'format' => $input->getOption('format'),
- 'namespace' => $input->getArgument('namespace'),
- 'raw_text' => $input->getOption('raw'),
- ]);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ foreach (['api', 'acsf'] as $prefix) {
+ if ($input->getArgument('namespace') !== $prefix) {
+ $allCommands = $this->getApplication()->all();
+ foreach ($allCommands as $command) {
+ if (
+ !is_a($command, ApiListCommandBase::class)
+ && !is_a($command, AcsfListCommandBase::class)
+ && str_starts_with($command->getName(), $prefix . ':')
+ ) {
+ $command->setHidden();
+ }
+ }
+ }
+ }
- return Command::SUCCESS;
- }
+ $helper = new DescriptorHelper();
+ $helper->describe($output, $this->getApplication(), [
+ 'format' => $input->getOption('format'),
+ 'namespace' => $input->getArgument('namespace'),
+ 'raw_text' => $input->getOption('raw'),
+ ]);
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Self/MakeDocsCommand.php b/src/Command/Self/MakeDocsCommand.php
index 17795c272..ad9896a5b 100644
--- a/src/Command/Self/MakeDocsCommand.php
+++ b/src/Command/Self/MakeDocsCommand.php
@@ -1,6 +1,6 @@
addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'The format to describe the docs in.', 'rst');
- $this->addOption('dump', 'd', InputOption::VALUE_OPTIONAL, 'Dump docs to directory');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $helper = new DescriptorHelper();
-
- if (!$input->getOption('dump')) {
- $helper->describe($output, $this->getApplication(), [
- 'format' => $input->getOption('format'),
- ]);
- return Command::SUCCESS;
+#[AsCommand(name: 'self:make-docs', description: 'Generate documentation for all ACLI commands', hidden: true)]
+final class MakeDocsCommand extends CommandBase
+{
+ protected function configure(): void
+ {
+ $this->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'The format to describe the docs in.', 'rst');
+ $this->addOption('dump', 'd', InputOption::VALUE_OPTIONAL, 'Dump docs to directory');
}
- $docs_dir = $input->getOption('dump');
- $this->localMachineHelper->getFilesystem()->mkdir($docs_dir);
- $buffer = new BufferedOutput();
- $helper->describe($buffer, $this->getApplication(), [
- 'format' => 'json',
- ]);
- $commands = json_decode($buffer->fetch(), TRUE);
- $index = [];
- foreach ($commands['commands'] as $command) {
- if ($command['definition']['hidden'] ?? FALSE) {
- continue;
- }
- $filename = $command['name'] . '.json';
- $index[] = [
- 'command' => $command['name'],
- 'help' => $command['help'],
- 'path' => $filename,
- 'usage' => $command['usage'][0],
- ];
- file_put_contents("$docs_dir/$filename", json_encode($command));
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $helper = new DescriptorHelper();
+
+ if (!$input->getOption('dump')) {
+ $helper->describe($output, $this->getApplication(), [
+ 'format' => $input->getOption('format'),
+ ]);
+ return Command::SUCCESS;
+ }
+
+ $docs_dir = $input->getOption('dump');
+ $this->localMachineHelper->getFilesystem()->mkdir($docs_dir);
+ $buffer = new BufferedOutput();
+ $helper->describe($buffer, $this->getApplication(), [
+ 'format' => 'json',
+ ]);
+ $commands = json_decode($buffer->fetch(), true);
+ $index = [];
+ foreach ($commands['commands'] as $command) {
+ if ($command['definition']['hidden'] ?? false) {
+ continue;
+ }
+ $filename = $command['name'] . '.json';
+ $index[] = [
+ 'command' => $command['name'],
+ 'help' => $command['help'],
+ 'path' => $filename,
+ 'usage' => $command['usage'][0],
+ ];
+ file_put_contents("$docs_dir/$filename", json_encode($command));
+ }
+ file_put_contents("$docs_dir/index.json", json_encode($index));
+ return Command::SUCCESS;
}
- file_put_contents("$docs_dir/index.json", json_encode($index));
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Self/TelemetryCommand.php b/src/Command/Self/TelemetryCommand.php
index 6ebc29aa2..2e6c71b99 100644
--- a/src/Command/Self/TelemetryCommand.php
+++ b/src/Command/Self/TelemetryCommand.php
@@ -1,6 +1,6 @@
datastoreCloud;
+ if ($datastore->get(DataStoreContract::SEND_TELEMETRY)) {
+ $datastore->set(DataStoreContract::SEND_TELEMETRY, false);
+ $this->io->success('Telemetry has been disabled.');
+ } else {
+ $datastore->set(DataStoreContract::SEND_TELEMETRY, true);
+ $this->io->success('Telemetry has been enabled.');
+ }
+ $oppositeVerb = $datastore->get(DataStoreContract::SEND_TELEMETRY) ? 'disable' : 'enable';
+ $this->io->writeln("Run this command again to $oppositeVerb telemetry");
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $datastore = $this->datastoreCloud;
- if ($datastore->get(DataStoreContract::SEND_TELEMETRY)) {
- $datastore->set(DataStoreContract::SEND_TELEMETRY, FALSE);
- $this->io->success('Telemetry has been disabled.');
+ return Command::SUCCESS;
}
- else {
- $datastore->set(DataStoreContract::SEND_TELEMETRY, TRUE);
- $this->io->success('Telemetry has been enabled.');
- }
- $oppositeVerb = $datastore->get(DataStoreContract::SEND_TELEMETRY) ? 'disable' : 'enable';
- $this->io->writeln("Run this command again to $oppositeVerb telemetry");
-
- return Command::SUCCESS;
- }
-
}
diff --git a/src/Command/Self/TelemetryDisableCommand.php b/src/Command/Self/TelemetryDisableCommand.php
index 2c757f031..91dc83c84 100644
--- a/src/Command/Self/TelemetryDisableCommand.php
+++ b/src/Command/Self/TelemetryDisableCommand.php
@@ -1,6 +1,6 @@
datastoreCloud;
- $datastore->set(DataStoreContract::SEND_TELEMETRY, FALSE);
- $this->io->success('Telemetry has been disabled.');
-
- return Command::SUCCESS;
- }
+final class TelemetryDisableCommand extends CommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $datastore = $this->datastoreCloud;
+ $datastore->set(DataStoreContract::SEND_TELEMETRY, false);
+ $this->io->success('Telemetry has been disabled.');
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Self/TelemetryEnableCommand.php b/src/Command/Self/TelemetryEnableCommand.php
index aec834d8e..cd9c9ec7f 100644
--- a/src/Command/Self/TelemetryEnableCommand.php
+++ b/src/Command/Self/TelemetryEnableCommand.php
@@ -1,6 +1,6 @@
datastoreCloud;
- $datastore->set(DataStoreContract::SEND_TELEMETRY, TRUE);
- $this->io->success('Telemetry has been enabled.');
-
- return Command::SUCCESS;
- }
+final class TelemetryEnableCommand extends CommandBase
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $datastore = $this->datastoreCloud;
+ $datastore->set(DataStoreContract::SEND_TELEMETRY, true);
+ $this->io->success('Telemetry has been enabled.');
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ssh/SshKeyCommandBase.php b/src/Command/Ssh/SshKeyCommandBase.php
index 8620f56fd..656ca01b9 100644
--- a/src/Command/Ssh/SshKeyCommandBase.php
+++ b/src/Command/Ssh/SshKeyCommandBase.php
@@ -1,6 +1,6 @@
privateSshKeyFilename = $privateSshKeyFilename;
+ $this->privateSshKeyFilepath = $this->sshDir . '/' . $this->privateSshKeyFilename;
+ $this->publicSshKeyFilepath = $this->privateSshKeyFilepath . '.pub';
+ }
- protected function setSshKeyFilepath(string $privateSshKeyFilename): void {
- $this->privateSshKeyFilename = $privateSshKeyFilename;
- $this->privateSshKeyFilepath = $this->sshDir . '/' . $this->privateSshKeyFilename;
- $this->publicSshKeyFilepath = $this->privateSshKeyFilepath . '.pub';
- }
+ protected static function getIdeSshKeyLabel(string $ideLabel, string $ideUuid): string
+ {
+ return self::normalizeSshKeyLabel('IDE_' . $ideLabel . '_' . $ideUuid);
+ }
- protected static function getIdeSshKeyLabel(string $ideLabel, string $ideUuid): string {
- return self::normalizeSshKeyLabel('IDE_' . $ideLabel . '_' . $ideUuid);
- }
+ private static function normalizeSshKeyLabel(?string $label): string|null
+ {
+ if (is_null($label)) {
+ throw new RuntimeException('The label cannot be empty');
+ }
+ // It may only contain letters, numbers and underscores.
+ return preg_replace('/\W/', '', $label);
+ }
- private static function normalizeSshKeyLabel(?string $label): string|null {
- if (is_null($label)) {
- throw new RuntimeException('The label cannot be empty');
+ /**
+ * Normalizes public SSH key by trimming and removing user and machine suffix.
+ */
+ protected function normalizePublicSshKey(string $publicKey): string
+ {
+ $parts = explode('== ', $publicKey);
+ $key = $parts[0];
+
+ return trim($key);
}
- // It may only contain letters, numbers and underscores.
- return preg_replace('/\W/', '', $label);
- }
-
- /**
- * Normalizes public SSH key by trimming and removing user and machine suffix.
- */
- protected function normalizePublicSshKey(string $publicKey): string {
- $parts = explode('== ', $publicKey);
- $key = $parts[0];
-
- return trim($key);
- }
-
- /**
- * Asserts whether ANY SSH key has been added to the local keychain.
- */
- protected function sshKeyIsAddedToKeychain(): bool {
- $process = $this->localMachineHelper->execute([
- 'ssh-add',
- '-L',
- ], NULL, NULL, FALSE);
-
- if ($process->isSuccessful()) {
- $keyContents = $this->normalizePublicSshKey($this->localMachineHelper->readFile($this->publicSshKeyFilepath));
- return str_contains($process->getOutput(), $keyContents);
+
+ /**
+ * Asserts whether ANY SSH key has been added to the local keychain.
+ */
+ protected function sshKeyIsAddedToKeychain(): bool
+ {
+ $process = $this->localMachineHelper->execute([
+ 'ssh-add',
+ '-L',
+ ], null, null, false);
+
+ if ($process->isSuccessful()) {
+ $keyContents = $this->normalizePublicSshKey($this->localMachineHelper->readFile($this->publicSshKeyFilepath));
+ return str_contains($process->getOutput(), $keyContents);
+ }
+ return false;
}
- return FALSE;
- }
-
- /**
- * Adds a given password protected local SSH key to the local keychain.
- *
- * @param string $filepath The filepath of the private SSH key.
- */
- protected function addSshKeyToAgent(string $filepath, string $password): void {
- // We must use a separate script to mimic user input due to the limitations of the `ssh-add` command.
- // @see https://www.linux.com/topic/networking/manage-ssh-key-file-passphrase/
- $tempFilepath = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli');
- $this->localMachineHelper->writeFile($tempFilepath, <<<'EOT'
+
+ /**
+ * Adds a given password protected local SSH key to the local keychain.
+ *
+ * @param string $filepath The filepath of the private SSH key.
+ */
+ protected function addSshKeyToAgent(string $filepath, string $password): void
+ {
+ // We must use a separate script to mimic user input due to the limitations of the `ssh-add` command.
+ // @see https://www.linux.com/topic/networking/manage-ssh-key-file-passphrase/
+ $tempFilepath = $this->localMachineHelper->getFilesystem()->tempnam(sys_get_temp_dir(), 'acli');
+ $this->localMachineHelper->writeFile($tempFilepath, <<<'EOT'
#!/usr/bin/env bash
echo $SSH_PASS
EOT
- );
- $this->localMachineHelper->getFilesystem()->chmod($tempFilepath, 0755);
- $privateKeyFilepath = str_replace('.pub', '', $filepath);
- $process = $this->localMachineHelper->executeFromCmd('SSH_PASS=' . $password . ' DISPLAY=1 SSH_ASKPASS=' . $tempFilepath . ' ssh-add ' . $privateKeyFilepath, NULL, NULL, FALSE);
- $this->localMachineHelper->getFilesystem()->remove($tempFilepath);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException('Unable to add the SSH key to local SSH agent:' . $process->getOutput() . $process->getErrorOutput());
- }
- }
-
- /**
- * Polls the Cloud Platform until a successful SSH request is made to the dev
- * environment.
- *
- * @infection-ignore-all
- */
- protected function pollAcquiaCloudUntilSshSuccess(
- OutputInterface $output
- ): void {
- // Create a loop to periodically poll the Cloud Platform.
- $timers = [];
- $startTime = time();
- $cloudAppUuid = $this->determineCloudApplication(TRUE);
- $permissions = $this->cloudApiClientService->getClient()->request('get', "/applications/$cloudAppUuid/permissions");
- $perms = array_column($permissions, 'name');
- $mappings = $this->checkPermissions($perms, $cloudAppUuid, $output);
- foreach ($mappings as $envName => $config) {
- $spinner = new Spinner($output, 4);
- $spinner->setMessage("Waiting for the key to become available in Cloud Platform $envName environments");
- $spinner->start();
- $mappings[$envName]['timer'] = Loop::addPeriodicTimer($spinner->interval(),
- static function () use ($spinner): void {
- $spinner->advance();
- });
- $mappings[$envName]['spinner'] = $spinner;
- }
- $callback = function () use ($output, &$mappings, &$timers, $startTime): void {
- foreach ($mappings as $envName => $config) {
- try {
- $process = $this->sshHelper->executeCommand($config['ssh_target'], ['ls'], FALSE);
- if (($process->getExitCode() === 128 && $envName === 'git') || $process->isSuccessful()) {
- // SSH key is available on this host, but may be pending on others.
- $config['spinner']->finish();
- Loop::cancelTimer($config['timer']);
- unset($mappings[$envName]);
- }
- else {
- // SSH key isn't available on this host... yet.
- $this->logger->debug($process->getOutput() . $process->getErrorOutput());
- }
- }
- catch (AcquiaCliException $exception) {
- $this->logger->debug($exception->getMessage());
- }
- }
- if (empty($mappings)) {
- // SSH key is available on every host.
- Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'success', 'duration' => time() - $startTime]);
- $output->writeln("\nYour SSH key is ready for use!\n");
- foreach ($timers as $timer) {
- Loop::cancelTimer($timer);
+ );
+ $this->localMachineHelper->getFilesystem()->chmod($tempFilepath, 0755);
+ $privateKeyFilepath = str_replace('.pub', '', $filepath);
+ $process = $this->localMachineHelper->executeFromCmd('SSH_PASS=' . $password . ' DISPLAY=1 SSH_ASKPASS=' . $tempFilepath . ' ssh-add ' . $privateKeyFilepath, null, null, false);
+ $this->localMachineHelper->getFilesystem()->remove($tempFilepath);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException('Unable to add the SSH key to local SSH agent:' . $process->getOutput() . $process->getErrorOutput());
}
+ }
+
+ /**
+ * Polls the Cloud Platform until a successful SSH request is made to the dev
+ * environment.
+ *
+ * @infection-ignore-all
+ */
+ protected function pollAcquiaCloudUntilSshSuccess(
+ OutputInterface $output
+ ): void {
+ // Create a loop to periodically poll the Cloud Platform.
$timers = [];
- }
- };
- // Poll Cloud every 5 seconds.
- $timers[] = Loop::addPeriodicTimer(5, $callback);
- $timers[] = Loop::addTimer(0.1, $callback);
- $timers[] = Loop::addTimer(60 * 60, static function () use ($output, &$timers): void {
- // Upload timed out.
- $output->writeln("\nThis is taking longer than usual. It will happen eventually!\n");
- Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'timeout']);
- foreach ($timers as $timer) {
- Loop::cancelTimer($timer);
- }
- $timers = [];
- });
- Loop::run();
- }
-
- /**
- * @return array
- */
- private function checkPermissions(array $userPerms, string $cloudAppUuid, OutputInterface $output): array {
- $mappings = [];
- $requiredPerms = ['add ssh key to git', 'add ssh key to non-prod', 'add ssh key to prod'];
- foreach ($requiredPerms as $index => $requiredPerm) {
- if (in_array($requiredPerm, $userPerms, TRUE)) {
- switch ($requiredPerm) {
- case 'add ssh key to git':
- $fullUrl = $this->getAnyVcsUrl($cloudAppUuid);
- $urlParts = explode(':', $fullUrl);
- $mappings['git']['ssh_target'] = $urlParts[0];
- break;
- case 'add ssh key to non-prod':
- if ($nonProdEnv = $this->getAnyNonProdAhEnvironment($cloudAppUuid)) {
- $mappings['nonprod']['ssh_target'] = $nonProdEnv->sshUrl;
+ $startTime = time();
+ $cloudAppUuid = $this->determineCloudApplication(true);
+ $permissions = $this->cloudApiClientService->getClient()->request('get', "/applications/$cloudAppUuid/permissions");
+ $perms = array_column($permissions, 'name');
+ $mappings = $this->checkPermissions($perms, $cloudAppUuid, $output);
+ foreach ($mappings as $envName => $config) {
+ $spinner = new Spinner($output, 4);
+ $spinner->setMessage("Waiting for the key to become available in Cloud Platform $envName environments");
+ $spinner->start();
+ $mappings[$envName]['timer'] = Loop::addPeriodicTimer(
+ $spinner->interval(),
+ static function () use ($spinner): void {
+ $spinner->advance();
+ }
+ );
+ $mappings[$envName]['spinner'] = $spinner;
+ }
+ $callback = function () use ($output, &$mappings, &$timers, $startTime): void {
+ foreach ($mappings as $envName => $config) {
+ try {
+ $process = $this->sshHelper->executeCommand($config['ssh_target'], ['ls'], false);
+ if (($process->getExitCode() === 128 && $envName === 'git') || $process->isSuccessful()) {
+ // SSH key is available on this host, but may be pending on others.
+ $config['spinner']->finish();
+ Loop::cancelTimer($config['timer']);
+ unset($mappings[$envName]);
+ } else {
+ // SSH key isn't available on this host... yet.
+ $this->logger->debug($process->getOutput() . $process->getErrorOutput());
+ }
+ } catch (AcquiaCliException $exception) {
+ $this->logger->debug($exception->getMessage());
+ }
}
- break;
- case 'add ssh key to prod':
- if ($prodEnv = $this->getAnyProdAhEnvironment($cloudAppUuid)) {
- $mappings['prod']['ssh_target'] = $prodEnv->sshUrl;
+ if (empty($mappings)) {
+ // SSH key is available on every host.
+ Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'success', 'duration' => time() - $startTime]);
+ $output->writeln("\nYour SSH key is ready for use!\n");
+ foreach ($timers as $timer) {
+ Loop::cancelTimer($timer);
+ }
+ $timers = [];
}
- break;
- }
- unset($requiredPerms[$index]);
- }
+ };
+ // Poll Cloud every 5 seconds.
+ $timers[] = Loop::addPeriodicTimer(5, $callback);
+ $timers[] = Loop::addTimer(0.1, $callback);
+ $timers[] = Loop::addTimer(60 * 60, static function () use ($output, &$timers): void {
+ // Upload timed out.
+ $output->writeln("\nThis is taking longer than usual. It will happen eventually!\n");
+ Amplitude::getInstance()->queueEvent('SSH key upload', ['result' => 'timeout']);
+ foreach ($timers as $timer) {
+ Loop::cancelTimer($timer);
+ }
+ $timers = [];
+ });
+ Loop::run();
}
- if (!empty($requiredPerms)) {
- $permString = implode(", ", $requiredPerms);
- $output->writeln('You do not have access to some environments on this application.');
- $output->writeln("Check that you have the following permissions: $permString>");
+
+ /**
+ * @return array
+ */
+ private function checkPermissions(array $userPerms, string $cloudAppUuid, OutputInterface $output): array
+ {
+ $mappings = [];
+ $requiredPerms = ['add ssh key to git', 'add ssh key to non-prod', 'add ssh key to prod'];
+ foreach ($requiredPerms as $index => $requiredPerm) {
+ if (in_array($requiredPerm, $userPerms, true)) {
+ switch ($requiredPerm) {
+ case 'add ssh key to git':
+ $fullUrl = $this->getAnyVcsUrl($cloudAppUuid);
+ $urlParts = explode(':', $fullUrl);
+ $mappings['git']['ssh_target'] = $urlParts[0];
+ break;
+ case 'add ssh key to non-prod':
+ if ($nonProdEnv = $this->getAnyNonProdAhEnvironment($cloudAppUuid)) {
+ $mappings['nonprod']['ssh_target'] = $nonProdEnv->sshUrl;
+ }
+ break;
+ case 'add ssh key to prod':
+ if ($prodEnv = $this->getAnyProdAhEnvironment($cloudAppUuid)) {
+ $mappings['prod']['ssh_target'] = $prodEnv->sshUrl;
+ }
+ break;
+ }
+ unset($requiredPerms[$index]);
+ }
+ }
+ if (!empty($requiredPerms)) {
+ $permString = implode(", ", $requiredPerms);
+ $output->writeln('You do not have access to some environments on this application.');
+ $output->writeln("Check that you have the following permissions: $permString>");
+ }
+ return $mappings;
}
- return $mappings;
- }
-
- protected function createSshKey(string $filename, string $password): string {
- $keyFilePath = $this->doCreateSshKey($filename, $password);
- $this->setSshKeyFilepath(basename($keyFilePath));
- if (!$this->sshKeyIsAddedToKeychain()) {
- $this->addSshKeyToAgent($this->publicSshKeyFilepath, $password);
+
+ protected function createSshKey(string $filename, string $password): string
+ {
+ $keyFilePath = $this->doCreateSshKey($filename, $password);
+ $this->setSshKeyFilepath(basename($keyFilePath));
+ if (!$this->sshKeyIsAddedToKeychain()) {
+ $this->addSshKeyToAgent($this->publicSshKeyFilepath, $password);
+ }
+ return $keyFilePath;
}
- return $keyFilePath;
- }
- private function doCreateSshKey(string $filename, string $password): string {
- $filepath = $this->sshDir . '/' . $filename;
- if (file_exists($filepath)) {
- throw new AcquiaCliException('An SSH key with the filename {filepath} already exists. Delete it and retry', ['filepath' => $filename]);
+ private function doCreateSshKey(string $filename, string $password): string
+ {
+ $filepath = $this->sshDir . '/' . $filename;
+ if (file_exists($filepath)) {
+ throw new AcquiaCliException('An SSH key with the filename {filepath} already exists. Delete it and retry', ['filepath' => $filename]);
+ }
+
+ $this->localMachineHelper->checkRequiredBinariesExist(['ssh-keygen']);
+ $process = $this->localMachineHelper->execute([
+ 'ssh-keygen',
+ '-t',
+ 'rsa',
+ '-b',
+ '4096',
+ '-f',
+ $filepath,
+ '-N',
+ $password,
+ ], null, null, false);
+ if (!$process->isSuccessful()) {
+ throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput());
+ }
+
+ return $filepath;
}
- $this->localMachineHelper->checkRequiredBinariesExist(['ssh-keygen']);
- $process = $this->localMachineHelper->execute([
- 'ssh-keygen',
- '-t',
- 'rsa',
- '-b',
- '4096',
- '-f',
- $filepath,
- '-N',
- $password,
- ], NULL, NULL, FALSE);
- if (!$process->isSuccessful()) {
- throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput());
+ protected function determineFilename(): string
+ {
+ return $this->determineOption(
+ 'filename',
+ false,
+ $this->validateFilename(...),
+ static function (mixed $value) {
+ return $value ? trim($value) : '';
+ },
+ 'id_rsa_acquia'
+ );
}
- return $filepath;
- }
-
- protected function determineFilename(): string {
- return $this->determineOption(
- 'filename',
- FALSE,
- $this->validateFilename(...),
- static function (mixed $value) {
- return $value ? trim($value) : '';},
- 'id_rsa_acquia'
- );
- }
-
- private function validateFilename(string $filename): string {
- $violations = Validation::createValidator()->validate($filename, [
- new Length(['min' => 5]),
- new NotBlank(),
- new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+ private function validateFilename(string $filename): string
+ {
+ $violations = Validation::createValidator()->validate($filename, [
+ new Length(['min' => 5]),
+ new NotBlank(),
+ new Regex(['pattern' => '/^\S*$/', 'message' => 'The value may not contain spaces']),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
+
+ return $filename;
}
- return $filename;
- }
-
- protected function determinePassword(): string {
- return $this->determineOption(
- 'password',
- TRUE,
- $this->validatePassword(...),
- static function (mixed $value) {
- return $value ? trim($value) : '';
- }
- );
- }
-
- private function validatePassword(string $password): string {
- $violations = Validation::createValidator()->validate($password, [
- new Length(['min' => 5]),
- new NotBlank(),
- ]);
- if (count($violations)) {
- throw new ValidatorException($violations->get(0)->getMessage());
+ protected function determinePassword(): string
+ {
+ return $this->determineOption(
+ 'password',
+ true,
+ $this->validatePassword(...),
+ static function (mixed $value) {
+ return $value ? trim($value) : '';
+ }
+ );
}
- return $password;
- }
+ private function validatePassword(string $password): string
+ {
+ $violations = Validation::createValidator()->validate($password, [
+ new Length(['min' => 5]),
+ new NotBlank(),
+ ]);
+ if (count($violations)) {
+ throw new ValidatorException($violations->get(0)->getMessage());
+ }
- private function keyHasUploaded(Client $acquiaCloudClient, string $publicKey): bool {
- $sshKeys = new SshKeys($acquiaCloudClient);
- foreach ($sshKeys->getAll() as $cloudKey) {
- if (trim($cloudKey->public_key) === trim($publicKey)) {
- return TRUE;
- }
+ return $password;
}
- return FALSE;
- }
-
- /**
- * @return array
- */
- protected function determinePublicSshKey(string $filepath = NULL): array {
- if ($filepath) {
- $filepath = $this->localMachineHelper->getLocalFilepath($filepath);
+
+ private function keyHasUploaded(Client $acquiaCloudClient, string $publicKey): bool
+ {
+ $sshKeys = new SshKeys($acquiaCloudClient);
+ foreach ($sshKeys->getAll() as $cloudKey) {
+ if (trim($cloudKey->public_key) === trim($publicKey)) {
+ return true;
+ }
+ }
+ return false;
}
- elseif ($this->input->hasOption('filepath') && $this->input->getOption('filepath')) {
- $filepath = $this->localMachineHelper->getLocalFilepath($this->input->getOption('filepath'));
+
+ /**
+ * @return array
+ */
+ protected function determinePublicSshKey(string $filepath = null): array
+ {
+ if ($filepath) {
+ $filepath = $this->localMachineHelper->getLocalFilepath($filepath);
+ } elseif ($this->input->hasOption('filepath') && $this->input->getOption('filepath')) {
+ $filepath = $this->localMachineHelper->getLocalFilepath($this->input->getOption('filepath'));
+ }
+
+ if ($filepath) {
+ if (!$this->localMachineHelper->getFilesystem()->exists($filepath)) {
+ throw new AcquiaCliException('The filepath {filepath} is not valid', ['filepath' => $filepath]);
+ }
+ if (!str_contains($filepath, '.pub')) {
+ throw new AcquiaCliException('The filepath {filepath} does not have the .pub extension', ['filepath' => $filepath]);
+ }
+ $publicKey = $this->localMachineHelper->readFile($filepath);
+ $chosenLocalKey = basename($filepath);
+ } else {
+ // Get local key and contents.
+ $localKeys = $this->findLocalSshKeys();
+ $chosenLocalKey = $this->promptChooseLocalSshKey($localKeys);
+ $publicKey = $this->getLocalSshKeyContents($localKeys, $chosenLocalKey);
+ }
+
+ return [$chosenLocalKey, $publicKey];
}
- if ($filepath) {
- if (!$this->localMachineHelper->getFilesystem()->exists($filepath)) {
- throw new AcquiaCliException('The filepath {filepath} is not valid', ['filepath' => $filepath]);
- }
- if (!str_contains($filepath, '.pub')) {
- throw new AcquiaCliException('The filepath {filepath} does not have the .pub extension', ['filepath' => $filepath]);
- }
- $publicKey = $this->localMachineHelper->readFile($filepath);
- $chosenLocalKey = basename($filepath);
+ private function promptChooseLocalSshKey(array $localKeys): string
+ {
+ $labels = [];
+ foreach ($localKeys as $localKey) {
+ $labels[] = $localKey->getFilename();
+ }
+ $question = new ChoiceQuestion(
+ 'Choose a local SSH key to upload to the Cloud Platform',
+ $labels
+ );
+ return $this->io->askQuestion($question);
}
- else {
- // Get local key and contents.
- $localKeys = $this->findLocalSshKeys();
- $chosenLocalKey = $this->promptChooseLocalSshKey($localKeys);
- $publicKey = $this->getLocalSshKeyContents($localKeys, $chosenLocalKey);
+
+ protected function determineSshKeyLabel(): string
+ {
+ return $this->determineOption('label', false, $this->validateSshKeyLabel(...), $this->normalizeSshKeyLabel(...));
}
- return [$chosenLocalKey, $publicKey];
- }
+ private function validateSshKeyLabel(mixed $label): mixed
+ {
+ if (trim($label) === '') {
+ throw new RuntimeException('The label cannot be empty');
+ }
- private function promptChooseLocalSshKey(array $localKeys): string {
- $labels = [];
- foreach ($localKeys as $localKey) {
- $labels[] = $localKey->getFilename();
+ return $label;
}
- $question = new ChoiceQuestion(
- 'Choose a local SSH key to upload to the Cloud Platform',
- $labels
- );
- return $this->io->askQuestion($question);
- }
-
- protected function determineSshKeyLabel(): string {
- return $this->determineOption('label', FALSE, $this->validateSshKeyLabel(...), $this->normalizeSshKeyLabel(...));
- }
-
- private function validateSshKeyLabel(mixed $label): mixed {
- if (trim($label) === '') {
- throw new RuntimeException('The label cannot be empty');
+
+ private function getLocalSshKeyContents(array $localKeys, string $chosenLocalKey): string
+ {
+ $filepath = '';
+ foreach ($localKeys as $localKey) {
+ if ($localKey->getFilename() === $chosenLocalKey) {
+ $filepath = $localKey->getRealPath();
+ break;
+ }
+ }
+ return $this->localMachineHelper->readFile($filepath);
}
- return $label;
- }
+ protected function uploadSshKey(string $label, string $publicKey): void
+ {
+ // @todo If a key with this label already exists, let the user try again.
+ $sshKeys = new SshKeys($this->cloudApiClientService->getClient());
+ $sshKeys->create($label, $publicKey);
+
+ // Wait for the key to register on the Cloud Platform.
+ if ($this->input->hasOption('no-wait') && $this->input->getOption('no-wait') === false) {
+ if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) {
+ $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.');
+ return;
+ }
- private function getLocalSshKeyContents(array $localKeys, string $chosenLocalKey): string {
- $filepath = '';
- foreach ($localKeys as $localKey) {
- if ($localKey->getFilename() === $chosenLocalKey) {
- $filepath = $localKey->getRealPath();
- break;
- }
- }
- return $this->localMachineHelper->readFile($filepath);
- }
-
- protected function uploadSshKey(string $label, string $publicKey): void {
- // @todo If a key with this label already exists, let the user try again.
- $sshKeys = new SshKeys($this->cloudApiClientService->getClient());
- $sshKeys->create($label, $publicKey);
-
- // Wait for the key to register on the Cloud Platform.
- if ($this->input->hasOption('no-wait') && $this->input->getOption('no-wait') === FALSE) {
- if ($this->input->isInteractive() && !$this->promptWaitForSsh($this->io)) {
- $this->io->success('Your SSH key has been successfully uploaded to the Cloud Platform.');
- return;
- }
-
- if ($this->keyHasUploaded($this->cloudApiClientService->getClient(), $publicKey)) {
- $this->pollAcquiaCloudUntilSshSuccess($this->output);
- }
+ if ($this->keyHasUploaded($this->cloudApiClientService->getClient(), $publicKey)) {
+ $this->pollAcquiaCloudUntilSshSuccess($this->output);
+ }
+ }
}
- }
- protected static function getFingerprint(mixed $sshPublicKey): string {
- if (!str_starts_with($sshPublicKey, 'ssh-rsa ')) {
- throw new AcquiaCliException('SSH keys must start with "ssh-rsa ".');
+ protected static function getFingerprint(mixed $sshPublicKey): string
+ {
+ if (!str_starts_with($sshPublicKey, 'ssh-rsa ')) {
+ throw new AcquiaCliException('SSH keys must start with "ssh-rsa ".');
+ }
+ $content = explode(' ', $sshPublicKey, 3);
+ return base64_encode(hash('sha256', base64_decode($content[1]), true));
}
- $content = explode(' ', $sshPublicKey, 3);
- return base64_encode(hash('sha256', base64_decode($content[1]), TRUE));
- }
-
}
diff --git a/src/Command/Ssh/SshKeyCreateCommand.php b/src/Command/Ssh/SshKeyCreateCommand.php
index 8b8892d91..0c88737a4 100644
--- a/src/Command/Ssh/SshKeyCreateCommand.php
+++ b/src/Command/Ssh/SshKeyCreateCommand.php
@@ -1,6 +1,6 @@
addOption('filename', NULL, InputOption::VALUE_REQUIRED, 'The filename of the SSH key')
- ->addOption('password', NULL, InputOption::VALUE_REQUIRED, 'The password for the SSH key');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $filename = $this->determineFilename();
- $password = $this->determinePassword();
- $this->createSshKey($filename, $password);
- $output->writeln('Created new SSH key. ' . $this->publicSshKeyFilepath);
-
- return Command::SUCCESS;
- }
-
+final class SshKeyCreateCommand extends SshKeyCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('filename', null, InputOption::VALUE_REQUIRED, 'The filename of the SSH key')
+ ->addOption('password', null, InputOption::VALUE_REQUIRED, 'The password for the SSH key');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $filename = $this->determineFilename();
+ $password = $this->determinePassword();
+ $this->createSshKey($filename, $password);
+ $output->writeln('Created new SSH key. ' . $this->publicSshKeyFilepath);
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ssh/SshKeyCreateUploadCommand.php b/src/Command/Ssh/SshKeyCreateUploadCommand.php
index f49b2c4a5..521f6f362 100644
--- a/src/Command/Ssh/SshKeyCreateUploadCommand.php
+++ b/src/Command/Ssh/SshKeyCreateUploadCommand.php
@@ -1,6 +1,6 @@
addOption('filename', NULL, InputOption::VALUE_REQUIRED, 'The filename of the SSH key')
- ->addOption('password', NULL, InputOption::VALUE_REQUIRED, 'The password for the SSH key')
- ->addOption('label', NULL, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform')
- ->addOption('no-wait', NULL, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform");
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $filename = $this->determineFilename();
- $password = $this->determinePassword();
- $this->createSshKey($filename, $password);
- $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath);
- $chosenLocalKey = basename($this->privateSshKeyFilepath);
- $label = $this->determineSshKeyLabel();
- $this->uploadSshKey($label, $publicKey);
- $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label");
-
- return Command::SUCCESS;
- }
-
+final class SshKeyCreateUploadCommand extends SshKeyCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('filename', null, InputOption::VALUE_REQUIRED, 'The filename of the SSH key')
+ ->addOption('password', null, InputOption::VALUE_REQUIRED, 'The password for the SSH key')
+ ->addOption('label', null, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform')
+ ->addOption('no-wait', null, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $filename = $this->determineFilename();
+ $password = $this->determinePassword();
+ $this->createSshKey($filename, $password);
+ $publicKey = $this->localMachineHelper->readFile($this->publicSshKeyFilepath);
+ $chosenLocalKey = basename($this->privateSshKeyFilepath);
+ $label = $this->determineSshKeyLabel();
+ $this->uploadSshKey($label, $publicKey);
+ $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label");
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/Ssh/SshKeyDeleteCommand.php b/src/Command/Ssh/SshKeyDeleteCommand.php
index 556a7dc94..47012ef3b 100644
--- a/src/Command/Ssh/SshKeyDeleteCommand.php
+++ b/src/Command/Ssh/SshKeyDeleteCommand.php
@@ -1,6 +1,6 @@
addOption('cloud-key-uuid', 'uuid', InputOption::VALUE_REQUIRED);
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- return $this->deleteSshKeyFromCloud($output);
- }
-
+final class SshKeyDeleteCommand extends SshKeyCommandBase
+{
+ use SshCommandTrait;
+
+ protected function configure(): void
+ {
+ $this
+ ->addOption('cloud-key-uuid', 'uuid', InputOption::VALUE_REQUIRED);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ return $this->deleteSshKeyFromCloud($output);
+ }
}
diff --git a/src/Command/Ssh/SshKeyInfoCommand.php b/src/Command/Ssh/SshKeyInfoCommand.php
index 06bf953f4..1c5a1f1a2 100644
--- a/src/Command/Ssh/SshKeyInfoCommand.php
+++ b/src/Command/Ssh/SshKeyInfoCommand.php
@@ -1,6 +1,6 @@
addOption('fingerprint', NULL, InputOption::VALUE_REQUIRED, 'sha256 fingerprint')
- ->addUsage('--fingerprint=pyarUa1mt2ln4fmrp7alWKpv1IPneqFwE+ErTC71IvY=');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $key = $this->determineSshKey($acquiaCloudClient);
-
- $location = 'Local';
- if (array_key_exists('cloud', $key)) {
- $location = array_key_exists('local', $key) ? 'Local + Cloud' : 'Cloud';
+final class SshKeyInfoCommand extends SshKeyCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('fingerprint', null, InputOption::VALUE_REQUIRED, 'sha256 fingerprint')
+ ->addUsage('--fingerprint=pyarUa1mt2ln4fmrp7alWKpv1IPneqFwE+ErTC71IvY=');
}
- $this->io->definitionList(
- ['SSH key property' => 'SSH key value'],
- new TableSeparator(),
- ['Location' => $location],
- ['Fingerprint (sha256)' => $key['fingerprint']],
- ['Fingerprint (md5)' => array_key_exists('cloud', $key) ? $key['cloud']['fingerprint'] : 'n/a'],
- ['UUID' => array_key_exists('cloud', $key) ? $key['cloud']['uuid'] : 'n/a'],
- ['Label' => array_key_exists('cloud', $key) ? $key['cloud']['label'] : $key['local']['filename']],
- ['Created at' => array_key_exists('cloud', $key) ? $key['cloud']['created_at'] : 'n/a'],
- );
- $this->io->writeln('Public key');
- $this->io->writeln('----------');
- $this->io->writeln($key['public_key']);
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $key = $this->determineSshKey($acquiaCloudClient);
- return Command::SUCCESS;
- }
+ $location = 'Local';
+ if (array_key_exists('cloud', $key)) {
+ $location = array_key_exists('local', $key) ? 'Local + Cloud' : 'Cloud';
+ }
+ $this->io->definitionList(
+ ['SSH key property' => 'SSH key value'],
+ new TableSeparator(),
+ ['Location' => $location],
+ ['Fingerprint (sha256)' => $key['fingerprint']],
+ ['Fingerprint (md5)' => array_key_exists('cloud', $key) ? $key['cloud']['fingerprint'] : 'n/a'],
+ ['UUID' => array_key_exists('cloud', $key) ? $key['cloud']['uuid'] : 'n/a'],
+ ['Label' => array_key_exists('cloud', $key) ? $key['cloud']['label'] : $key['local']['filename']],
+ ['Created at' => array_key_exists('cloud', $key) ? $key['cloud']['created_at'] : 'n/a'],
+ );
- /**
- * @return array
- */
- private function determineSshKey(mixed $acquiaCloudClient): array {
- $cloudKeysResponse = new SshKeys($acquiaCloudClient);
- $cloudKeys = $cloudKeysResponse->getAll();
- $localKeys = $this->findLocalSshKeys();
- $keys = [];
- /** @var \AcquiaCloudApi\Response\SshKeyResponse $key */
- foreach ($cloudKeys as $key) {
- $fingerprint = self::getFingerprint($key->public_key);
- $keys[$fingerprint]['fingerprint'] = $fingerprint;
- $keys[$fingerprint]['public_key'] = $key->public_key;
- $keys[$fingerprint]['cloud'] = [
- 'created_at' => $key->created_at,
- 'fingerprint' => $key->fingerprint,
- 'label' => $key->label,
- 'uuid' => $key->uuid,
- ];
- }
- foreach ($localKeys as $key) {
- $fingerprint = self::getFingerprint($key->getContents());
- $keys[$fingerprint]['fingerprint'] = $fingerprint;
- $keys[$fingerprint]['public_key'] = $key->getContents();
- $keys[$fingerprint]['local'] = [
- 'filename' => $key->getFilename(),
- ];
- }
- if ($fingerprint = $this->input->getOption('fingerprint')) {
- if (!array_key_exists($fingerprint, $keys)) {
- throw new AcquiaCliException('No key exists matching provided fingerprint');
- }
- return $keys[$fingerprint];
- }
+ $this->io->writeln('Public key');
+ $this->io->writeln('----------');
+ $this->io->writeln($key['public_key']);
- return $this->promptChooseFromObjectsOrArrays(
- $keys,
- 'fingerprint',
- 'fingerprint',
- 'Choose an SSH key to view'
- );
+ return Command::SUCCESS;
+ }
- }
+ /**
+ * @return array
+ */
+ private function determineSshKey(mixed $acquiaCloudClient): array
+ {
+ $cloudKeysResponse = new SshKeys($acquiaCloudClient);
+ $cloudKeys = $cloudKeysResponse->getAll();
+ $localKeys = $this->findLocalSshKeys();
+ $keys = [];
+ /** @var \AcquiaCloudApi\Response\SshKeyResponse $key */
+ foreach ($cloudKeys as $key) {
+ $fingerprint = self::getFingerprint($key->public_key);
+ $keys[$fingerprint]['fingerprint'] = $fingerprint;
+ $keys[$fingerprint]['public_key'] = $key->public_key;
+ $keys[$fingerprint]['cloud'] = [
+ 'created_at' => $key->created_at,
+ 'fingerprint' => $key->fingerprint,
+ 'label' => $key->label,
+ 'uuid' => $key->uuid,
+ ];
+ }
+ foreach ($localKeys as $key) {
+ $fingerprint = self::getFingerprint($key->getContents());
+ $keys[$fingerprint]['fingerprint'] = $fingerprint;
+ $keys[$fingerprint]['public_key'] = $key->getContents();
+ $keys[$fingerprint]['local'] = [
+ 'filename' => $key->getFilename(),
+ ];
+ }
+ if ($fingerprint = $this->input->getOption('fingerprint')) {
+ if (!array_key_exists($fingerprint, $keys)) {
+ throw new AcquiaCliException('No key exists matching provided fingerprint');
+ }
+ return $keys[$fingerprint];
+ }
+ return $this->promptChooseFromObjectsOrArrays(
+ $keys,
+ 'fingerprint',
+ 'fingerprint',
+ 'Choose an SSH key to view'
+ );
+ }
}
diff --git a/src/Command/Ssh/SshKeyListCommand.php b/src/Command/Ssh/SshKeyListCommand.php
index cdb165aa6..3f711a408 100644
--- a/src/Command/Ssh/SshKeyListCommand.php
+++ b/src/Command/Ssh/SshKeyListCommand.php
@@ -1,6 +1,6 @@
cloudApiClientService->getClient();
+ $sshKeys = new SshKeys($acquiaCloudClient);
+ $cloudKeys = $sshKeys->getAll();
+ $localKeys = $this->findLocalSshKeys();
+ $table = $this->createSshKeyTable($output, 'Cloud Platform keys with matching local keys');
+ foreach ($localKeys as $localIndex => $localFile) {
+ /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */
+ foreach ($cloudKeys as $index => $cloudKey) {
+ if (trim($localFile->getContents()) === trim($cloudKey->public_key)) {
+ $hash = self::getFingerprint($cloudKey->public_key);
+ $table->addRow([
+ $cloudKey->label,
+ $localFile->getFilename(),
+ $hash,
+ ]);
+ unset($cloudKeys[$index], $localKeys[$localIndex]);
+ break;
+ }
+ }
+ }
+ $table->render();
+ $this->io->newLine();
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $sshKeys = new SshKeys($acquiaCloudClient);
- $cloudKeys = $sshKeys->getAll();
- $localKeys = $this->findLocalSshKeys();
- $table = $this->createSshKeyTable($output, 'Cloud Platform keys with matching local keys');
- foreach ($localKeys as $localIndex => $localFile) {
- /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */
- foreach ($cloudKeys as $index => $cloudKey) {
- if (trim($localFile->getContents()) === trim($cloudKey->public_key)) {
- $hash = self::getFingerprint($cloudKey->public_key);
- $table->addRow([
+ $table = $this->createSshKeyTable($output, 'Cloud Platform keys with no matching local keys');
+ foreach ($cloudKeys as $cloudKey) {
+ $hash = self::getFingerprint($cloudKey->public_key);
+ $table->addRow([
$cloudKey->label,
+ 'none',
+ $hash,
+ ]);
+ }
+ $table->render();
+ $this->io->newLine();
+
+ $table = $this->createSshKeyTable($output, 'Local keys with no matching Cloud Platform keys');
+ foreach ($localKeys as $localFile) {
+ $hash = self::getFingerprint($localFile->getContents());
+ $table->addRow([
+ 'none',
$localFile->getFilename(),
$hash,
- ]);
- unset($cloudKeys[$index], $localKeys[$localIndex]);
- break;
+ ]);
}
- }
- }
- $table->render();
- $this->io->newLine();
+ $table->render();
- $table = $this->createSshKeyTable($output, 'Cloud Platform keys with no matching local keys');
- foreach ($cloudKeys as $cloudKey) {
- $hash = self::getFingerprint($cloudKey->public_key);
- $table->addRow([
- $cloudKey->label,
- 'none',
- $hash,
- ]);
+ return Command::SUCCESS;
}
- $table->render();
- $this->io->newLine();
- $table = $this->createSshKeyTable($output, 'Local keys with no matching Cloud Platform keys');
- foreach ($localKeys as $localFile) {
- $hash = self::getFingerprint($localFile->getContents());
- $table->addRow([
- 'none',
- $localFile->getFilename(),
- $hash,
- ]);
+ private function createSshKeyTable(OutputInterface $output, string $title): Table
+ {
+ $headers = ['Cloud Platform label', 'Local filename', 'Fingerprint (sha256)'];
+ $widths = [.4, .2, .2];
+ return $this->createTable($output, $title, $headers, $widths);
}
- $table->render();
-
- return Command::SUCCESS;
- }
-
- private function createSshKeyTable(OutputInterface $output, string $title): Table {
- $headers = ['Cloud Platform label', 'Local filename', 'Fingerprint (sha256)'];
- $widths = [.4, .2, .2];
- return $this->createTable($output, $title, $headers, $widths);
- }
-
}
diff --git a/src/Command/Ssh/SshKeyUploadCommand.php b/src/Command/Ssh/SshKeyUploadCommand.php
index cc55f90a7..191c5e9e2 100644
--- a/src/Command/Ssh/SshKeyUploadCommand.php
+++ b/src/Command/Ssh/SshKeyUploadCommand.php
@@ -1,6 +1,6 @@
addOption('filepath', NULL, InputOption::VALUE_REQUIRED, 'The filepath of the public SSH key to upload')
- ->addOption('label', NULL, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform')
- ->addOption('no-wait', NULL, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform");
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- [$chosenLocalKey, $publicKey] = $this->determinePublicSshKey();
- $label = $this->determineSshKeyLabel();
- $this->uploadSshKey($label, $publicKey);
- $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label");
-
- return Command::SUCCESS;
- }
-
+final class SshKeyUploadCommand extends SshKeyCommandBase
+{
+ protected function configure(): void
+ {
+ $this
+ ->addOption('filepath', null, InputOption::VALUE_REQUIRED, 'The filepath of the public SSH key to upload')
+ ->addOption('label', null, InputOption::VALUE_REQUIRED, 'The SSH key label to be used with the Cloud Platform')
+ ->addOption('no-wait', null, InputOption::VALUE_NONE, "Don't wait for the SSH key to be uploaded to the Cloud Platform");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ [$chosenLocalKey, $publicKey] = $this->determinePublicSshKey();
+ $label = $this->determineSshKeyLabel();
+ $this->uploadSshKey($label, $publicKey);
+ $this->io->success("Uploaded $chosenLocalKey to the Cloud Platform with label $label");
+
+ return Command::SUCCESS;
+ }
}
diff --git a/src/Command/WizardCommandBase.php b/src/Command/WizardCommandBase.php
index 25891b497..ccf773f16 100644
--- a/src/Command/WizardCommandBase.php
+++ b/src/Command/WizardCommandBase.php
@@ -1,6 +1,6 @@
getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) {
+ $commandName = 'auth:login';
+ $command = $this->getApplication()->find($commandName);
+ $arguments = ['command' => $commandName];
+ $createInput = new ArrayInput($arguments);
+ $exitCode = $command->run($createInput, $output);
+ if ($exitCode !== 0) {
+ throw new AcquiaCliException("Unable to authenticate with the Cloud Platform.");
+ }
+ }
+ $this->validateEnvironment();
- protected function initialize(InputInterface $input, OutputInterface $output): void {
- if ((new \ReflectionClass(static::class))->getAttributes(RequireAuth::class) && !$this->cloudApiClientService->isMachineAuthenticated()) {
- $commandName = 'auth:login';
- $command = $this->getApplication()->find($commandName);
- $arguments = ['command' => $commandName];
- $createInput = new ArrayInput($arguments);
- $exitCode = $command->run($createInput, $output);
- if ($exitCode !== 0) {
- throw new AcquiaCliException("Unable to authenticate with the Cloud Platform.");
- }
+ parent::initialize($input, $output);
}
- $this->validateEnvironment();
- parent::initialize($input, $output);
- }
-
- protected function deleteLocalSshKey(): void {
- $this->localMachineHelper->getFilesystem()->remove([
- $this->publicSshKeyFilepath,
- $this->privateSshKeyFilepath,
- ]);
- }
-
- protected function savePassPhraseToFile(string $passphrase): bool|int {
- return file_put_contents($this->passphraseFilepath, $passphrase);
- }
+ protected function deleteLocalSshKey(): void
+ {
+ $this->localMachineHelper->getFilesystem()->remove([
+ $this->publicSshKeyFilepath,
+ $this->privateSshKeyFilepath,
+ ]);
+ }
- protected function getPassPhraseFromFile(): string {
- return file_get_contents($this->passphraseFilepath);
- }
+ protected function savePassPhraseToFile(string $passphrase): bool|int
+ {
+ return file_put_contents($this->passphraseFilepath, $passphrase);
+ }
- /**
- * Assert whether ANY local key exists that has a corresponding key on the
- * Cloud Platform.
- */
- protected function userHasUploadedThisKeyToCloud(string $label): bool {
- $acquiaCloudClient = $this->cloudApiClientService->getClient();
- $sshKeys = new SshKeys($acquiaCloudClient);
- $cloudKeys = $sshKeys->getAll();
- /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */
- foreach ($cloudKeys as $index => $cloudKey) {
- if (
- $cloudKey->label === $label
- // Assert that a corresponding local key exists.
- && $this->localSshKeyExists()
- // Assert local public key contents match Cloud public key contents.
- && $this->normalizePublicSshKey($cloudKey->public_key) === $this->normalizePublicSshKey(file_get_contents($this->publicSshKeyFilepath))
- ) {
- return TRUE;
- }
+ protected function getPassPhraseFromFile(): string
+ {
+ return file_get_contents($this->passphraseFilepath);
}
- return FALSE;
- }
- protected function passPhraseFileExists(): bool {
- return file_exists($this->passphraseFilepath);
- }
+ /**
+ * Assert whether ANY local key exists that has a corresponding key on the
+ * Cloud Platform.
+ */
+ protected function userHasUploadedThisKeyToCloud(string $label): bool
+ {
+ $acquiaCloudClient = $this->cloudApiClientService->getClient();
+ $sshKeys = new SshKeys($acquiaCloudClient);
+ $cloudKeys = $sshKeys->getAll();
+ /** @var \AcquiaCloudApi\Response\SshKeyResponse $cloudKey */
+ foreach ($cloudKeys as $index => $cloudKey) {
+ if (
+ $cloudKey->label === $label
+ // Assert that a corresponding local key exists.
+ && $this->localSshKeyExists()
+ // Assert local public key contents match Cloud public key contents.
+ && $this->normalizePublicSshKey($cloudKey->public_key) === $this->normalizePublicSshKey(file_get_contents($this->publicSshKeyFilepath))
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
- protected function localSshKeyExists(): bool {
- return file_exists($this->publicSshKeyFilepath) && file_exists($this->privateSshKeyFilepath);
- }
+ protected function passPhraseFileExists(): bool
+ {
+ return file_exists($this->passphraseFilepath);
+ }
+ protected function localSshKeyExists(): bool
+ {
+ return file_exists($this->publicSshKeyFilepath) && file_exists($this->privateSshKeyFilepath);
+ }
}
diff --git a/src/CommandFactoryInterface.php b/src/CommandFactoryInterface.php
index 33205ab46..b8d830578 100644
--- a/src/CommandFactoryInterface.php
+++ b/src/CommandFactoryInterface.php
@@ -1,6 +1,6 @@
getRootNode()
+ public function getConfigTreeBuilder(): TreeBuilder
+ {
+ $treeBuilder = new TreeBuilder('acquia_cli');
+ $treeBuilder
+ ->getRootNode()
->children()
->scalarNode('cloud_app_uuid')
->end()
- ->end();
- return $treeBuilder;
- }
-
+ ->end();
+ return $treeBuilder;
+ }
}
diff --git a/src/Config/CloudDataConfig.php b/src/Config/CloudDataConfig.php
index d283cebb6..61d89e3f4 100644
--- a/src/Config/CloudDataConfig.php
+++ b/src/Config/CloudDataConfig.php
@@ -1,23 +1,25 @@
getRootNode();
- $rootNode
- ->children()
+ public function getConfigTreeBuilder(): TreeBuilder
+ {
+ $treeBuilder = new TreeBuilder('cloud_api');
+ $rootNode = $treeBuilder->getRootNode();
+ $rootNode
+ ->children()
// I can't find a better node type that accepts TRUE, FALSE, and NULL.
// boolNode() will cast NULL to FALSE and enumNode()->values() will
@@ -28,7 +30,7 @@ public function getConfigTreeBuilder(): TreeBuilder {
->arrayNode('keys')
->useAttributeAsKey('uuid')
- ->normalizeKeys(FALSE)
+ ->normalizeKeys(false)
->arrayPrototype()
->children()
->scalarNode('label')->end()
@@ -42,14 +44,14 @@ public function getConfigTreeBuilder(): TreeBuilder {
->children()
->scalarNode('uuid')->end()
->booleanNode('is_acquian')
- ->defaultValue(FALSE)
+ ->defaultValue(false)
->end()
->end()
->end()
->arrayNode('acsf_factories')
->useAttributeAsKey('url')
- ->normalizeKeys(FALSE)
+ ->normalizeKeys(false)
->arrayPrototype()
->children()
->arrayNode('users')
@@ -68,13 +70,12 @@ public function getConfigTreeBuilder(): TreeBuilder {
->scalarNode('acsf_active_factory')->end()
- ->end()
- ->validate()
- ->ifTrue(function ($config) {
- return is_array($config['keys']) && !empty($config['keys']) && !array_key_exists($config['acli_key'], $config['keys']);
- })
- ->thenInvalid('acli_key must exist in keys');
- return $treeBuilder;
- }
-
+ ->end()
+ ->validate()
+ ->ifTrue(function ($config) {
+ return is_array($config['keys']) && !empty($config['keys']) && !array_key_exists($config['acli_key'], $config['keys']);
+ })
+ ->thenInvalid('acli_key must exist in keys');
+ return $treeBuilder;
+ }
}
diff --git a/src/ConnectorFactoryInterface.php b/src/ConnectorFactoryInterface.php
index 88f5e2d90..ded5c0138 100644
--- a/src/ConnectorFactoryInterface.php
+++ b/src/ConnectorFactoryInterface.php
@@ -1,14 +1,13 @@
- */
- protected array $config;
-
- public function __construct(
- protected LocalMachineHelper $localMachineHelper,
- AcquiaCliConfig $configDefinition,
- string $acliConfigFilepath
- ) {
- $filePath = $localMachineHelper->getLocalFilepath($acliConfigFilepath);
- parent::__construct($filePath, $configDefinition);
- }
+class AcquiaCliDatastore extends YamlStore
+{
+ /**
+ * @var array
+ */
+ protected array $config;
+ public function __construct(
+ protected LocalMachineHelper $localMachineHelper,
+ AcquiaCliConfig $configDefinition,
+ string $acliConfigFilepath
+ ) {
+ $filePath = $localMachineHelper->getLocalFilepath($acliConfigFilepath);
+ parent::__construct($filePath, $configDefinition);
+ }
}
diff --git a/src/DataStore/CloudDataStore.php b/src/DataStore/CloudDataStore.php
index 22995e4d4..ac8b1aa16 100644
--- a/src/DataStore/CloudDataStore.php
+++ b/src/DataStore/CloudDataStore.php
@@ -1,25 +1,24 @@
- */
- protected array $config;
-
- public function __construct(
- protected LocalMachineHelper $localMachineHelper,
- CloudDataConfig $cloudDataConfig,
- string $cloudConfigFilepath
- ) {
- parent::__construct($cloudConfigFilepath, $cloudDataConfig);
- }
+class CloudDataStore extends JsonDataStore
+{
+ /**
+ * @var array
+ */
+ protected array $config;
+ public function __construct(
+ protected LocalMachineHelper $localMachineHelper,
+ CloudDataConfig $cloudDataConfig,
+ string $cloudConfigFilepath
+ ) {
+ parent::__construct($cloudConfigFilepath, $cloudDataConfig);
+ }
}
diff --git a/src/DataStore/DataStoreInterface.php b/src/DataStore/DataStoreInterface.php
index 17cc9f359..71bbc8033 100644
--- a/src/DataStore/DataStoreInterface.php
+++ b/src/DataStore/DataStoreInterface.php
@@ -1,19 +1,18 @@
fileSystem = new Filesystem();
- $this->filepath = $path;
- $this->expander = new Expander();
- $this->expander->setStringifier(new Stringifier());
- $this->data = new Data();
- }
-
- public function set(string $key, mixed $value): void {
- $this->data->set($key, $value);
- $this->dump();
- }
-
- public function get(string $key): mixed {
- try {
- return $this->data->get($key);
- }
- catch (MissingPathException) {
- return NULL;
+ public function __construct(string $path)
+ {
+ $this->fileSystem = new Filesystem();
+ $this->filepath = $path;
+ $this->expander = new Expander();
+ $this->expander->setStringifier(new Stringifier());
+ $this->data = new Data();
}
- }
- public function remove(string $key): void {
- $this->data->remove($key);
- $this->dump();
- }
+ public function set(string $key, mixed $value): void
+ {
+ $this->data->set($key, $value);
+ $this->dump();
+ }
- public function exists(string $key): bool {
- return $this->data->has($key);
- }
+ public function get(string $key): mixed
+ {
+ try {
+ return $this->data->get($key);
+ } catch (MissingPathException) {
+ return null;
+ }
+ }
- /**
- * @param string $path Path to the datastore on disk.
- * @return array