From f5fda09eac544dfb454ae3dfe9dd82ac5da3772d Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Wed, 18 Dec 2024 20:20:15 +0000 Subject: [PATCH] Fix some phpstan level 7 errors --- .../Activity/ActivityCancelCommand.php | 6 +- src/Command/Auth/AuthInfoCommand.php | 4 - src/Command/Auth/BrowserLoginCommand.php | 3 +- src/Command/Backup/BackupCreateCommand.php | 2 +- src/Command/Backup/BackupGetCommand.php | 3 +- src/Command/Db/DbSqlCommand.php | 3 +- src/Command/Domain/DomainCommandBase.php | 2 +- src/Command/Domain/DomainGetCommand.php | 3 +- .../Environment/EnvironmentBranchCommand.php | 3 +- .../EnvironmentCheckoutCommand.php | 2 +- .../Environment/EnvironmentLogCommand.php | 3 +- .../Environment/EnvironmentUrlCommand.php | 3 +- .../IntegrationActivityGetCommand.php | 1 - .../Integration/IntegrationCommandBase.php | 16 ++-- .../Integration/IntegrationListCommand.php | 2 +- src/Command/LegacyMigrateCommand.php | 5 +- src/Command/Local/LocalBuildCommand.php | 2 +- .../Local/LocalDrushAliasesCommand.php | 6 +- src/Command/Metrics/DiskUsageCommand.php | 2 + src/Command/Metrics/MetricsCommandBase.php | 13 +-- src/Command/Mount/MountDownloadCommand.php | 1 + src/Command/Mount/MountListCommand.php | 2 +- src/Command/Mount/MountSizeCommand.php | 16 ++-- src/Command/MultiCommand.php | 33 ++++--- .../Billing/OrganizationAddressCommand.php | 5 +- .../Organization/OrganizationInfoCommand.php | 3 + .../User/OrganizationUserCommandBase.php | 1 + src/Command/Project/ProjectCreateCommand.php | 85 +++++++++++-------- src/Command/Project/ProjectDeleteCommand.php | 2 +- src/Command/Project/ProjectInfoCommand.php | 2 +- .../Project/ProjectSetRemoteCommand.php | 6 +- src/Command/Resources/ResourcesSetCommand.php | 18 ++-- .../Resources/ResourcesSizeListCommand.php | 3 +- src/Command/Route/RouteGetCommand.php | 5 +- src/Command/Route/RouteListCommand.php | 2 +- src/Command/RuntimeOperation/ListCommand.php | 2 +- src/Command/Self/SelfBuildCommand.php | 6 +- src/Command/Self/SelfInstallCommand.php | 7 +- src/Command/Self/SelfReleaseCommand.php | 12 +-- src/Command/Self/SelfStatsCommand.php | 2 +- src/Command/Server/ServerCommandBase.php | 49 ++++++++--- .../Service/MongoDB/MongoDumpCommand.php | 8 +- .../Service/MongoDB/MongoExportCommand.php | 9 +- src/Command/Session/SessionSwitchCommand.php | 3 +- src/Command/SourceOperation/ListCommand.php | 6 +- src/Command/SourceOperation/RunCommand.php | 7 +- src/Command/SshKey/SshKeyAddCommand.php | 12 +-- src/Command/SshKey/SshKeyDeleteCommand.php | 5 +- src/Command/SubscriptionInfoCommand.php | 2 +- .../Team/Project/TeamProjectAddCommand.php | 2 + src/Command/Team/TeamCommandBase.php | 2 +- src/Command/Team/TeamCreateCommand.php | 36 ++++---- src/Command/Tunnel/TunnelOpenCommand.php | 3 +- src/Command/Tunnel/TunnelSingleCommand.php | 3 +- src/Command/User/UserAddCommand.php | 15 ++-- src/Command/User/UserListCommand.php | 2 +- src/Console/ArrayArgument.php | 8 +- src/CredentialHelper/Manager.php | 4 +- src/CredentialHelper/SessionStorage.php | 2 +- src/Local/LocalProject.php | 2 +- src/Model/EnvironmentDomain.php | 1 + src/Model/Metrics/Sketch.php | 10 +++ src/Service/Api.php | 39 ++++++--- src/Service/QuestionHelper.php | 10 +-- src/Service/Relationships.php | 29 ++++--- src/Service/RemoteEnvVars.php | 2 +- src/Service/Rsync.php | 2 +- src/Service/Shell.php | 37 +++++--- src/SshCert/Certifier.php | 2 +- 69 files changed, 355 insertions(+), 254 deletions(-) diff --git a/src/Command/Activity/ActivityCancelCommand.php b/src/Command/Activity/ActivityCancelCommand.php index 660f9916e..c76453018 100644 --- a/src/Command/Activity/ActivityCancelCommand.php +++ b/src/Command/Activity/ActivityCancelCommand.php @@ -81,20 +81,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } $choices = []; - $questionHelper = $this->questionHelper; - $formatter = $this->propertyFormatter; $byId = []; $this->api->sortResources($activities, 'created_at'); foreach ($activities as $activity) { $byId[$activity->id] = $activity; $choices[$activity->id] = \sprintf( '%s: %s (%s)', - $formatter->formatDate($activity->created_at), + $this->propertyFormatter->formatDate($activity->created_at), ActivityMonitor::getFormattedDescription($activity), ActivityMonitor::formatState($activity->state) ); } - $id = $questionHelper->choose($choices, 'Enter a number to choose an activity to cancel:', key($choices), true); + $id = $this->questionHelper->choose($choices, 'Enter a number to choose an activity to cancel:', (string) key($choices)); $activity = $byId[$id]; } diff --git a/src/Command/Auth/AuthInfoCommand.php b/src/Command/Auth/AuthInfoCommand.php index b1ee1b085..b076b5d38 100644 --- a/src/Command/Auth/AuthInfoCommand.php +++ b/src/Command/Auth/AuthInfoCommand.php @@ -57,10 +57,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Exit early if it's the user ID. if ($property === 'id') { $userId = $this->api->getMyUserId($input->getOption('refresh')); - if ($userId === false) { - $this->stdErr->writeln('The current session is not associated with a user ID'); - return 1; - } $output->writeln($userId); return 0; } diff --git a/src/Command/Auth/BrowserLoginCommand.php b/src/Command/Auth/BrowserLoginCommand.php index 2a7326118..3ad563740 100644 --- a/src/Command/Auth/BrowserLoginCommand.php +++ b/src/Command/Auth/BrowserLoginCommand.php @@ -88,8 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $account['email'] )); - $questionHelper = $this->questionHelper; - if (!$questionHelper->confirm('Log in anyway?', false)) { + if (!$this->questionHelper->confirm('Log in anyway?', false)) { return 1; } $force = true; diff --git a/src/Command/Backup/BackupCreateCommand.php b/src/Command/Backup/BackupCreateCommand.php index d04c19932..0084e8104 100644 --- a/src/Command/Backup/BackupCreateCommand.php +++ b/src/Command/Backup/BackupCreateCommand.php @@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln('The environment is not active.'); } else { try { - if ($this->isUserAdmin($selection->getProject(), $selectedEnvironment, (string) $this->api->getMyUserId())) { + if ($this->isUserAdmin($selection->getProject(), $selectedEnvironment, $this->api->getMyUserId())) { $this->stdErr->writeln('You must be an administrator to create a backup.'); } } catch (\Exception $e) { diff --git a/src/Command/Backup/BackupGetCommand.php b/src/Command/Backup/BackupGetCommand.php index 72afc6c8a..4d2c91f90 100644 --- a/src/Command/Backup/BackupGetCommand.php +++ b/src/Command/Backup/BackupGetCommand.php @@ -58,8 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $choices[$id] = sprintf('%s (%s)', $backup->id, $this->propertyFormatter->format($backup->created_at, 'created_at')); } - $questionHelper = $this->questionHelper; - $choice = $questionHelper->choose($choices, 'Enter a number to choose a backup:', $default); + $choice = $this->questionHelper->choose($choices, 'Enter a number to choose a backup:', $default); $backup = $byId[$choice]; } diff --git a/src/Command/Db/DbSqlCommand.php b/src/Command/Db/DbSqlCommand.php index eb86dee55..648f3c6f2 100644 --- a/src/Command/Db/DbSqlCommand.php +++ b/src/Command/Db/DbSqlCommand.php @@ -105,8 +105,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $choices[$schema] .= ' (default)'; } } - $questionHelper = $this->questionHelper; - $schema = $questionHelper->choose($choices, 'Enter a number to choose a schema:', $default, true); + $schema = $this->questionHelper->choose($choices, 'Enter a number to choose a schema:', $default, true); $schema = $schema === '(none)' ? '' : $schema; } } diff --git a/src/Command/Domain/DomainCommandBase.php b/src/Command/Domain/DomainCommandBase.php index 846d9f61d..a756a89bd 100644 --- a/src/Command/Domain/DomainCommandBase.php +++ b/src/Command/Domain/DomainCommandBase.php @@ -151,7 +151,7 @@ protected function validateDomainInput(InputInterface $input, Selection $selecti . "\nA non-production domain must be attached to an existing production domain." . "\nIt will inherit the same routing behavior." . "\nChoose a production domain:"; - $this->attach = (string) $this->questionHelper->choose($choices, $questionText, $default); + $this->attach = $this->questionHelper->choose($choices, $questionText, $default); } } elseif ($this->attach !== null) { try { diff --git a/src/Command/Domain/DomainGetCommand.php b/src/Command/Domain/DomainGetCommand.php index e0e965874..0eb326f70 100644 --- a/src/Command/Domain/DomainGetCommand.php +++ b/src/Command/Domain/DomainGetCommand.php @@ -68,8 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $options[$domain->name] = $domain->name; $byName[$domain->name] = $domain; } - $questionHelper = $this->questionHelper; - $domainName = $questionHelper->choose($options, 'Enter a number to choose a domain:'); + $domainName = $this->questionHelper->choose($options, 'Enter a number to choose a domain:'); $domain = $byName[$domainName]; } diff --git a/src/Command/Environment/EnvironmentBranchCommand.php b/src/Command/Environment/EnvironmentBranchCommand.php index 4b7663346..5545eacfb 100644 --- a/src/Command/Environment/EnvironmentBranchCommand.php +++ b/src/Command/Environment/EnvironmentBranchCommand.php @@ -103,8 +103,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $questionHelper = $this->questionHelper; - $checkout = $questionHelper->confirm( + $checkout = $this->questionHelper->confirm( "The environment $branchName already exists. Check out?" ); if ($checkout) { diff --git a/src/Command/Environment/EnvironmentCheckoutCommand.php b/src/Command/Environment/EnvironmentCheckoutCommand.php index 222a7461c..c4a8b3c79 100644 --- a/src/Command/Environment/EnvironmentCheckoutCommand.php +++ b/src/Command/Environment/EnvironmentCheckoutCommand.php @@ -159,7 +159,7 @@ protected function offerBranchChoice(Project $project, string $projectRoot): str // The environment ID will be an integer if it was numeric // (because PHP does that with array keys), so it's cast back to // a string here. - return (string) $this->questionHelper->choose($environmentList, $chooseEnvironmentText); + return $this->questionHelper->choose($environmentList, $chooseEnvironmentText); } // If there's only one choice, QuestionHelper::choose() does not diff --git a/src/Command/Environment/EnvironmentLogCommand.php b/src/Command/Environment/EnvironmentLogCommand.php index b972039d3..3f2f773e3 100644 --- a/src/Command/Environment/EnvironmentLogCommand.php +++ b/src/Command/Environment/EnvironmentLogCommand.php @@ -78,7 +78,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln('No log type specified.'); return 1; } else { - $questionHelper = $this->questionHelper; // Read the list of files from the environment. $cacheKey = sprintf('log-files:%s', $host->getCacheKey()); @@ -102,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Ask the user to choose a file. $files = array_combine($files, array_map(fn($file): string => str_replace('.log', '', basename(trim((string) $file))), $files)); - $logFilename = $questionHelper->choose($files, 'Enter a number to choose a log: '); + $logFilename = $this->questionHelper->choose($files, 'Enter a number to choose a log: '); } $command = sprintf('tail -n %1$d %2$s', $input->getOption('lines'), $logFilename); diff --git a/src/Command/Environment/EnvironmentUrlCommand.php b/src/Command/Environment/EnvironmentUrlCommand.php index 9077d8b83..7f6c8b690 100644 --- a/src/Command/Environment/EnvironmentUrlCommand.php +++ b/src/Command/Environment/EnvironmentUrlCommand.php @@ -120,8 +120,7 @@ private function displayOrOpenUrls(array $urls, InputInterface $input, OutputInt if (count($urls) === 1) { $url = $urls[0]; } else { - $questionHelper = $this->questionHelper; - $url = (string) $questionHelper->choose(array_combine($urls, $urls), 'Enter a number to open a URL', $urls[0]); + $url = $this->questionHelper->choose(array_combine($urls, $urls), 'Enter a number to open a URL', $urls[0]); } $this->url->openUrl($url); diff --git a/src/Command/Integration/Activity/IntegrationActivityGetCommand.php b/src/Command/Integration/Activity/IntegrationActivityGetCommand.php index 44e87dd66..a90c3b723 100644 --- a/src/Command/Integration/Activity/IntegrationActivityGetCommand.php +++ b/src/Command/Integration/Activity/IntegrationActivityGetCommand.php @@ -10,7 +10,6 @@ use Platformsh\Cli\Service\ActivityMonitor; use Platformsh\Cli\Service\PropertyFormatter; use Platformsh\Cli\Service\Table; -use Platformsh\Client\Model\Activity; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; diff --git a/src/Command/Integration/IntegrationCommandBase.php b/src/Command/Integration/IntegrationCommandBase.php index aeac16acc..48b7e7903 100644 --- a/src/Command/Integration/IntegrationCommandBase.php +++ b/src/Command/Integration/IntegrationCommandBase.php @@ -36,6 +36,7 @@ abstract class IntegrationCommandBase extends CommandBase private ?Form $form = null; + /** @var array */ private array $bitbucketAccessTokens = []; protected ?Selection $selection = null; @@ -63,12 +64,11 @@ protected function selectIntegration(Project $project, ?string $id, bool $intera return false; } - $questionHelper = $this->questionHelper; $choices = []; foreach ($integrations as $integration) { $choices[$integration->id] = sprintf('%s (%s)', $integration->id, $integration->type); } - $id = $questionHelper->choose($choices, 'Enter a number to choose an integration:'); + $id = $this->questionHelper->choose($choices, 'Enter a number to choose an integration:'); } $integration = $project->getIntegration($id); @@ -118,10 +118,10 @@ protected function handleConditionalFieldException(ConditionalFieldException $e) /** * Performs extra logic on values after the form is complete. * - * @param array $values + * @param array $values * @param Integration|null $integration * - * @return array + * @return array */ protected function postProcessValues(array $values, ?Integration $integration = null): array { @@ -170,7 +170,7 @@ protected function postProcessValues(array $values, ?Integration $integration = /** * Returns a list of integration capability information on the selected project, if any. * - * @return array + * @return array{enabled: bool, config?: array} */ private function selectedProjectIntegrations(): array { @@ -658,6 +658,8 @@ protected function displayIntegration(Integration $integration): void /** * Obtains an OAuth2 token for Bitbucket from the given app credentials. + * + * @param array{key: string, secret: string} $credentials */ protected function getBitbucketAccessToken(array $credentials): string { @@ -685,6 +687,8 @@ protected function getBitbucketAccessToken(array $credentials): string /** * Validates Bitbucket credentials. + * + * @param array{key: string, secret: string} $credentials */ protected function validateBitbucketCredentials(array $credentials): true|string { @@ -705,6 +709,8 @@ protected function validateBitbucketCredentials(array $credentials): true|string /** * Lists validation errors found in an integration. + * + * @param array $errors */ protected function listValidationErrors(array $errors, OutputInterface $output): void { diff --git a/src/Command/Integration/IntegrationListCommand.php b/src/Command/Integration/IntegrationListCommand.php index 2d85dc472..88c9da3ba 100644 --- a/src/Command/Integration/IntegrationListCommand.php +++ b/src/Command/Integration/IntegrationListCommand.php @@ -129,7 +129,7 @@ protected function getIntegrationSummary(Integration $integration): string break; default: - $summary = json_encode($details); + $summary = json_encode($details, JSON_THROW_ON_ERROR); } if (strlen($summary) > 240) { diff --git a/src/Command/LegacyMigrateCommand.php b/src/Command/LegacyMigrateCommand.php index 86bc0ef5c..bcd59de63 100644 --- a/src/Command/LegacyMigrateCommand.php +++ b/src/Command/LegacyMigrateCommand.php @@ -68,8 +68,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new RootNotFoundException(); } - $cwd = getcwd(); - $repositoryDir = $legacyRoot . '/repository'; if (!is_dir($repositoryDir)) { $this->stdErr->writeln('Directory not found: ' . $repositoryDir . ''); @@ -158,7 +156,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln("\nMigration complete\n"); - if (str_starts_with($cwd, $repositoryDir)) { + $cwd = getcwd(); + if ($cwd !== false && str_starts_with($cwd, $repositoryDir)) { $this->stdErr->writeln('Type this to refresh your shell:'); $this->stdErr->writeln(' cd ' . $legacyRoot . ''); } diff --git a/src/Command/Local/LocalBuildCommand.php b/src/Command/Local/LocalBuildCommand.php index a37ce4a08..1a5d03c6b 100644 --- a/src/Command/Local/LocalBuildCommand.php +++ b/src/Command/Local/LocalBuildCommand.php @@ -137,7 +137,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($sourceDirOption) { $sourceDir = realpath($sourceDirOption); - if (!is_dir($sourceDir)) { + if ($sourceDir === false || !is_dir($sourceDir)) { throw new InvalidArgumentException('Source directory not found: ' . $sourceDirOption); } diff --git a/src/Command/Local/LocalDrushAliasesCommand.php b/src/Command/Local/LocalDrushAliasesCommand.php index a0b0c6067..15c7d35b0 100644 --- a/src/Command/Local/LocalDrushAliasesCommand.php +++ b/src/Command/Local/LocalDrushAliasesCommand.php @@ -92,7 +92,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->isInteractive()) { - $this->migrateAliasFiles($this->drush); + $this->migrateAliasFiles(); } $aliases = $this->drush->getAliases($current_group); @@ -210,7 +210,7 @@ protected function ensureDrushConfig(): void $drushYml = $this->drush->getDrushDir() . '/drush.yml'; $drushConfig = []; if (file_exists($drushYml)) { - $drushConfig = (array) Yaml::parse(file_get_contents($drushYml)); + $drushConfig = (array) Yaml::parse((string) file_get_contents($drushYml)); } $aliasPath = $this->drush->getSiteAliasDir(); if (getenv('HOME')) { @@ -233,7 +233,7 @@ protected function ensureDrushConfig(): void /** * Migrates old alias file(s) from ~/.drush to ~/.drush/site-aliases. */ - protected function migrateAliasFiles(Drush $drush): void + protected function migrateAliasFiles(): void { $newDrushDir = $this->drush->getHomeDir() . '/.drush/site-aliases'; $oldFilenames = $this->drush->getLegacyAliasFiles(); diff --git a/src/Command/Metrics/DiskUsageCommand.php b/src/Command/Metrics/DiskUsageCommand.php index 9081a38b6..4909fde4e 100644 --- a/src/Command/Metrics/DiskUsageCommand.php +++ b/src/Command/Metrics/DiskUsageCommand.php @@ -35,7 +35,9 @@ class DiskUsageCommand extends MetricsCommandBase ]; /** @var string[] */ private array $defaultColumns = ['timestamp', 'service', 'used', 'limit', 'percent', 'ipercent', 'tmp_percent']; + /** @var string[] */ private array $tmpReportColumns = ['timestamp', 'service', 'tmp_used', 'tmp_limit', 'tmp_percent', 'tmp_ipercent']; + public function __construct(private readonly PropertyFormatter $propertyFormatter, private readonly Selector $selector, private readonly Table $table) { parent::__construct(); diff --git a/src/Command/Metrics/MetricsCommandBase.php b/src/Command/Metrics/MetricsCommandBase.php index aa1c8e2f7..f319184ce 100644 --- a/src/Command/Metrics/MetricsCommandBase.php +++ b/src/Command/Metrics/MetricsCommandBase.php @@ -44,6 +44,7 @@ abstract class MetricsCommandBase extends CommandBase */ private bool $foundHighMemoryServices = false; + /** @var array> */ private array $fields = [ // Grid. 'local' => [ @@ -189,7 +190,7 @@ private function dimensionFields(string $dimension): array * @param string[] $fieldNames * An array of field names, which map to queries in $this->fields. * - * @return false|array + * @return false|array>>> * False on failure, or an array of sketch values, keyed by: time, service, dimension, and name. */ protected function fetchMetrics(InputInterface $input, TimeSpec $timeSpec, Environment $environment, array $fieldNames): array|false @@ -275,7 +276,7 @@ protected function fetchMetrics(InputInterface $input, TimeSpec $timeSpec, Envir $client = $this->api->getHttpClient(); $request = new Request('POST', $metricsQueryUrl, [ 'Content-Type' => 'application/json', - ], json_encode($query->asArray())); + ], json_encode($query->asArray(), JSON_THROW_ON_ERROR)); try { $result = $client->send($request); } catch (BadResponseException $e) { @@ -458,12 +459,12 @@ private function getDeploymentType(Environment $environment): string /** * Builds metrics table rows. * - * @param array $values + * @param array>>> $values * An array of values from fetchMetrics(). * @param array $fields * An array of fields keyed by column name. * - * @return array + * @return array|TableSeparator> * Table rows. */ protected function buildRows(array $values, array $fields, Environment $environment): array @@ -542,8 +543,8 @@ protected function buildRows(array $values, array $fields, Environment $environm /** * Merges table rows per service to reduce unnecessary empty cells. * - * @param array $rows - * @return array + * @param array|TableSeparator> $rows + * @return array|TableSeparator> */ private function mergeRows(array $rows): array { diff --git a/src/Command/Mount/MountDownloadCommand.php b/src/Command/Mount/MountDownloadCommand.php index b87d2d921..ae246cf64 100644 --- a/src/Command/Mount/MountDownloadCommand.php +++ b/src/Command/Mount/MountDownloadCommand.php @@ -24,6 +24,7 @@ #[AsCommand(name: 'mount:download', description: 'Download files from a mount, using rsync')] class MountDownloadCommand extends CommandBase { + /** @var LocalApplication[]|null */ private ?array $localApps = null; public function __construct(private readonly ApplicationFinder $applicationFinder, private readonly Config $config, private readonly Filesystem $filesystem, private readonly Mount $mount, private readonly QuestionHelper $questionHelper, private readonly Rsync $rsync, private readonly Selector $selector) diff --git a/src/Command/Mount/MountListCommand.php b/src/Command/Mount/MountListCommand.php index dd0ff35be..8fc57e565 100644 --- a/src/Command/Mount/MountListCommand.php +++ b/src/Command/Mount/MountListCommand.php @@ -48,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (($applicationEnv = getenv($this->config->getStr('service.env_prefix') . 'APPLICATION')) && !LocalHost::conflictsWithCommandLineOptions($input, $this->config->getStr('service.env_prefix'))) { $this->io->debug('Selected host: localhost'); - $config = json_decode(base64_decode($applicationEnv), true) ?: []; + $config = json_decode((string) base64_decode($applicationEnv), true) ?: []; $mounts = $this->mount->mountsFromConfig(new AppConfig($config)); $appName = $config['name']; $appType = str_contains((string) $appName, '--') ? 'worker' : 'app'; diff --git a/src/Command/Mount/MountSizeCommand.php b/src/Command/Mount/MountSizeCommand.php index a316a3faa..18f0d8c62 100644 --- a/src/Command/Mount/MountSizeCommand.php +++ b/src/Command/Mount/MountSizeCommand.php @@ -1,4 +1,5 @@ selector->getSelection($input, new SelectorConfig(allowLocalHost: getenv($this->config->getStr('service.env_prefix') . 'APPLICATION'))); + $selection = $this->selector->getSelection($input, new SelectorConfig( + allowLocalHost: getenv($this->config->getStr('service.env_prefix') . 'APPLICATION') !== false, + )); $host = $this->selector->getHostFromSelection($input, $selection); if ($host instanceof LocalHost) { $envVars = $this->remoteEnvVars; @@ -216,9 +219,9 @@ private function getDfColumn(string $line, string $columnName): string * * @param string $dfOutput * @param string $appDir - * @param array $mountPaths + * @param string[] $mountPaths * - * @return array + * @return array */ private function parseDf(string $dfOutput, string $appDir, array $mountPaths): array { @@ -261,9 +264,9 @@ private function parseDf(string $dfOutput, string $appDir, array $mountPaths): a * Parse the 'du' output. * * @param string $duOutput - * @param array $mountPaths + * @param string[] $mountPaths * - * @return array A list of mount sizes (in bytes) keyed by mount path. + * @return array A list of mount sizes (in bytes) keyed by mount path. */ private function parseDu(string $duOutput, array $mountPaths): array { @@ -273,7 +276,8 @@ private function parseDu(string $duOutput, array $mountPaths): array if (!isset($duOutputSplit[$i])) { throw new \RuntimeException("Failed to find row $i of 'du' command output: \n" . $duOutput); } - list($mountSizes[$mountPath],) = explode("\t", $duOutputSplit[$i], 2); + $parts = explode("\t", $duOutputSplit[$i], 2); + $mountSizes[$mountPath] = (int) $parts[0]; } return $mountSizes; diff --git a/src/Command/MultiCommand.php b/src/Command/MultiCommand.php index 1320985da..9d9e762a1 100644 --- a/src/Command/MultiCommand.php +++ b/src/Command/MultiCommand.php @@ -34,7 +34,7 @@ protected function configure(): void { $this ->addArgument('cmd', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'The command to execute') - ->addOption('projects', 'p', InputOption::VALUE_REQUIRED, 'A list of project IDs, separated by commas and/or whitespace') + ->addOption('projects', 'p', InputOption::VALUE_REQUIRED, 'A list of project IDs. ' . ArrayArgument::SPLIT_HELP) ->addOption('continue', null, InputOption::VALUE_NONE, 'Continue running commands even if an exception is encountered') ->addOption('sort', null, InputOption::VALUE_REQUIRED, 'A property by which to sort the list of project options', 'title') ->addOption('reverse', null, InputOption::VALUE_NONE, 'Reverse the order of project options'); @@ -111,6 +111,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Shows a checklist using the dialog utility. + * + * @param array $options An array of project labels keyed by ID. + * + * @return string[] A list of project IDs. */ protected function showDialogChecklist(array $options, string $text = 'Choose item(s)'): array { @@ -125,7 +129,7 @@ protected function showDialogChecklist(array $options, string $text = 'Choose it $listHeight ); foreach ($options as $tag => $option) { - $command .= sprintf(' %s %s off', escapeshellarg($tag), escapeshellarg((string) $option)); + $command .= sprintf(' %s %s off', escapeshellarg($tag), escapeshellarg($option)); } $dialogRc = file_get_contents(CLI_ROOT . '/resources/console/dialogrc'); @@ -138,9 +142,12 @@ protected function showDialogChecklist(array $options, string $text = 'Choose it $process = proc_open($command, [ 2 => array('pipe', 'w'), ], $pipes); + if (!$process) { + throw new \RuntimeException('Failed to start dialog command: ' . $process); + } // Wait for and read result. - $result = array_filter(explode("\n", trim(stream_get_contents($pipes[2])))); + $result = array_filter(explode("\n", trim((string) stream_get_contents($pipes[2])))); // Close handles. if (is_resource($pipes[2])) { @@ -188,12 +195,12 @@ protected function getAllProjectsBasicInfo(InputInterface $input): array */ protected function getSelectedProjects(InputInterface $input): false|array { - $projectList = $input->getOption('projects'); + $projectList = ArrayArgument::getOption($input, 'projects'); if (!empty($projectList)) { $missing = []; $selected = []; - foreach ($this->splitProjectList($projectList) as $projectId) { + foreach ($projectList as $projectId) { try { $result = $this->identifier->identify($projectId); } catch (InvalidArgumentException) { @@ -239,14 +246,12 @@ protected function getSelectedProjects(InputInterface $input): false|array $this->stdErr->writeln('Selected project(s): ' . implode(',', $selected)); $this->stdErr->writeln(''); - return array_map(fn($id) => $this->api->getProject($id), $selected); - } - - /** - * Splits a list of project IDs. - */ - private function splitProjectList(string $list): array - { - return array_filter(array_unique(preg_split('/[,\s]+/', $list) ?: [])); + return array_map(function ($id) { + $project = $this->api->getProject($id); + if (!$project) { + throw new \RuntimeException('Failed to fetch project: ' . $id); + } + return $project; + }, $selected); } } diff --git a/src/Command/Organization/Billing/OrganizationAddressCommand.php b/src/Command/Organization/Billing/OrganizationAddressCommand.php index 988f1831f..8d4373508 100644 --- a/src/Command/Organization/Billing/OrganizationAddressCommand.php +++ b/src/Command/Organization/Billing/OrganizationAddressCommand.php @@ -81,6 +81,9 @@ protected function display(Address $address, Organization $org, InputInterface $ } } + /** + * @return array + */ protected function parseUpdates(InputInterface $input): array { $property = $input->getArgument('property'); @@ -117,7 +120,7 @@ protected function parseUpdates(InputInterface $input): array } /** - * @param array $updates + * @param array $updates * @param Address $address * * @return int diff --git a/src/Command/Organization/OrganizationInfoCommand.php b/src/Command/Organization/OrganizationInfoCommand.php index 87e703141..f07127a22 100644 --- a/src/Command/Organization/OrganizationInfoCommand.php +++ b/src/Command/Organization/OrganizationInfoCommand.php @@ -57,6 +57,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->setProperty($property, $value, $organization); } + /** + * @return array + */ private function getProperties(Organization $organization): array { $data = $organization->getProperties(); diff --git a/src/Command/Organization/User/OrganizationUserCommandBase.php b/src/Command/Organization/User/OrganizationUserCommandBase.php index ecbed32b1..12ccb4094 100644 --- a/src/Command/Organization/User/OrganizationUserCommandBase.php +++ b/src/Command/Organization/User/OrganizationUserCommandBase.php @@ -9,6 +9,7 @@ class OrganizationUserCommandBase extends OrganizationCommandBase { // @todo add 'admin' + /** @var string[] */ protected static array $allPermissions = ['billing', 'members', 'plans', 'projects:create', 'projects:list']; /** diff --git a/src/Command/Project/ProjectCreateCommand.php b/src/Command/Project/ProjectCreateCommand.php index 1aeed47c0..ab6831af8 100644 --- a/src/Command/Project/ProjectCreateCommand.php +++ b/src/Command/Project/ProjectCreateCommand.php @@ -20,8 +20,10 @@ use Platformsh\Cli\Util\OsUtil; use Platformsh\Cli\Util\Sort; use Platformsh\Client\Model\Organization\Organization; +use Platformsh\Client\Model\Project; use Platformsh\Client\Model\Region; use Platformsh\Client\Model\SetupOptions; +use Platformsh\Client\Model\Subscription; use Platformsh\Client\Model\Subscription\SubscriptionOptions; use Platformsh\ConsoleForm\Field\Field; use Platformsh\ConsoleForm\Field\OptionsField; @@ -35,7 +37,9 @@ #[AsCommand(name: 'project:create', description: 'Create a new project', aliases: ['create'])] class ProjectCreateCommand extends CommandBase { + /** @var string[]|null */ private ?array $plansCache = null; + /** @var Region[]|null */ private ?array $regionsCache = null; public function __construct(private readonly Api $api, private readonly Config $config, private readonly Git $git, private readonly Io $io, private readonly LocalProject $localProject, private readonly QuestionHelper $questionHelper, private readonly Selector $selector, private readonly SubCommandRunner $subCommandRunner) @@ -271,18 +275,57 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } + $project = $this->waitForProject($subscription, $totalTimeout, $start); + if (!$project) { + return 1; + } + + $this->stdErr->writeln("The project is now ready!"); + $output->writeln($subscription->project_id); + $this->stdErr->writeln(''); + + $this->stdErr->writeln(" Region: {$subscription->project_region}"); + $this->stdErr->writeln(" Project ID: {$subscription->project_id}"); + $this->stdErr->writeln(" Project title: {$subscription->project_title}"); + $this->stdErr->writeln(''); + + $this->stdErr->writeln(sprintf(" Console URL: %s", $this->api->getConsoleURL($project))); + + $this->stdErr->writeln(" Git URL: {$project->getGitUrl()}"); + + if ($setRemote && $gitRoot !== false) { + $this->stdErr->writeln(''); + $this->stdErr->writeln(sprintf( + 'Setting the remote project for this repository to: %s', + $this->api->getProjectLabel($project) + )); + + $localProject = $this->localProject; + $localProject->mapDirectory($gitRoot, $project); + } + + if ($gitRoot === false) { + $this->stdErr->writeln(''); + $this->stdErr->writeln(sprintf('To clone the project locally, run: %s get %s', $this->config->getStr('application.executable'), OsUtil::escapeShellArg($project->id))); + } + + return 0; + } + + private function waitForProject(Subscription $subscription, int|float $totalTimeout, float $start): Project|false + { $progressMessage = new ProgressMessage($this->stdErr); $checkInterval = 1; $lastCheck = time(); $progressMessage->show('Loading project information...'); - $project = false; while (true) { if (time() - $lastCheck >= $checkInterval) { $lastCheck = time(); try { $project = $this->api->getProject($subscription->project_id); if ($project !== false) { - break; + $progressMessage->done(); + return $project; } else { $this->io->debug(sprintf('Project not found: %s (retrying)', $subscription->project_id)); } @@ -305,41 +348,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $progressMessage->done(); $this->stdErr->writeln(sprintf('The subscription is active but the project %s could not be fetched.', $subscription->project_id)); $this->stdErr->writeln('The project may be accessible momentarily. Otherwise, please contact support.'); - return 1; + return false; } } - $progressMessage->done(); - - $this->stdErr->writeln("The project is now ready!"); - $output->writeln($subscription->project_id); - $this->stdErr->writeln(''); - - $this->stdErr->writeln(" Region: {$subscription->project_region}"); - $this->stdErr->writeln(" Project ID: {$subscription->project_id}"); - $this->stdErr->writeln(" Project title: {$subscription->project_title}"); - $this->stdErr->writeln(''); - - $this->stdErr->writeln(sprintf(" Console URL: %s", $this->api->getConsoleURL($project))); - - $this->stdErr->writeln(" Git URL: {$project->getGitUrl()}"); - - if ($setRemote && $gitRoot !== false) { - $this->stdErr->writeln(''); - $this->stdErr->writeln(sprintf( - 'Setting the remote project for this repository to: %s', - $this->api->getProjectLabel($project) - )); - - $localProject = $this->localProject; - $localProject->mapDirectory($gitRoot, $project); - } - - if ($gitRoot === false) { - $this->stdErr->writeln(''); - $this->stdErr->writeln(sprintf('To clone the project locally, run: %s get %s', $this->config->getStr('application.executable'), OsUtil::escapeShellArg($project->id))); - } - - return 0; } /** @@ -424,7 +435,7 @@ private function requireVerification(string $type, string $message, InputInterfa * * @param SetupOptions|null $setupOptions * - * @return array + * @return string[] * A list of plan machine names. */ protected function getAvailablePlans(?SetupOptions $setupOptions = null): array @@ -444,6 +455,8 @@ protected function getAvailablePlans(?SetupOptions $setupOptions = null): array /** * Picks a default plan from a list. + * + * @param string[] $availablePlans */ protected function getDefaultPlan(array $availablePlans): ?string { diff --git a/src/Command/Project/ProjectDeleteCommand.php b/src/Command/Project/ProjectDeleteCommand.php index 3eceeb9d3..37f9cdd33 100644 --- a/src/Command/Project/ProjectDeleteCommand.php +++ b/src/Command/Project/ProjectDeleteCommand.php @@ -42,7 +42,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $selection = $this->selector->getSelection($input); $project = $selection->getProject(); - $subscriptionId = $project->getSubscriptionId(); + $subscriptionId = (string) $project->getSubscriptionId(); $subscription = $this->api->loadSubscription($subscriptionId, $project); if (!$subscription) { $this->stdErr->writeln('Subscription not found: ' . $subscriptionId . ''); diff --git a/src/Command/Project/ProjectInfoCommand.php b/src/Command/Project/ProjectInfoCommand.php index ac067ec58..5b8c29845 100644 --- a/src/Command/Project/ProjectInfoCommand.php +++ b/src/Command/Project/ProjectInfoCommand.php @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param array $properties + * @param array $properties * * @return int */ diff --git a/src/Command/Project/ProjectSetRemoteCommand.php b/src/Command/Project/ProjectSetRemoteCommand.php index 4f4514771..db1fbb5c8 100644 --- a/src/Command/Project/ProjectSetRemoteCommand.php +++ b/src/Command/Project/ProjectSetRemoteCommand.php @@ -47,7 +47,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $result = $identifier->identify($projectId); $projectId = $result['projectId']; } - $root = $this->git->getRoot(getcwd()); + $cwd = getcwd(); + if (!$cwd) { + throw new \RuntimeException('Failed to find current working directory'); + } + $root = $this->git->getRoot($cwd); if ($root === false) { $this->stdErr->writeln( 'No Git repository found. Use git init to create a repository.' diff --git a/src/Command/Resources/ResourcesSetCommand.php b/src/Command/Resources/ResourcesSetCommand.php index a1b6f189d..420cc63d9 100644 --- a/src/Command/Resources/ResourcesSetCommand.php +++ b/src/Command/Resources/ResourcesSetCommand.php @@ -274,6 +274,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $project = $selection->getProject(); $organization = $this->api->getClient()->getOrganizationById($project->getProperty('organization')); + if (!$organization) { + throw new \RuntimeException('Failed to load project organization: ' . $project->getProperty('organization')); + } $profile = $organization->getProfile(); if ($input->getOption('force') === false && isset($profile->resources_limit) && $profile->resources_limit) { $diff = $this->computeMemoryCPUStorageDiff($updates, $current); @@ -342,9 +345,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Summarizes all the changes that would be made. * - * @param array $updates + * @param array>> $updates * @param array $services - * @param array $containerProfiles + * @param array $containerProfiles * @return void */ private function summarizeChanges(array $updates, array $services, array $containerProfiles): void @@ -359,6 +362,9 @@ private function summarizeChanges(array $updates, array $services, array $contai /** * Summarizes changes per service. + * + * @param array $updates + * @param array $containerProfiles */ private function summarizeChangesPerService(string $name, WebApp|Worker|Service $service, array $updates, array $containerProfiles): void { @@ -583,7 +589,7 @@ private function parseSetting(InputInterface $input, string $optionName, array $ /** * Print errors found after parsing a setting. * - * @param array $errors + * @param string[] $errors * @param string $optionName * * @return string[] @@ -610,10 +616,10 @@ private function formatErrors(array $errors, string $optionName): array * Compute the total memory/CPU/storage diff that will occur when the given update * is applied. * - * @param array $updates - * @param array $current + * @param array>> $updates + * @param array>> $current * - * @return array + * @return array{memory: int|float, cpu: int|float, disk: int|float} */ private function computeMemoryCPUStorageDiff(array $updates, array $current): array { diff --git a/src/Command/Resources/ResourcesSizeListCommand.php b/src/Command/Resources/ResourcesSizeListCommand.php index 11af222c3..3bf44c8ec 100644 --- a/src/Command/Resources/ResourcesSizeListCommand.php +++ b/src/Command/Resources/ResourcesSizeListCommand.php @@ -71,12 +71,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } } elseif ($input->isInteractive()) { - $questionHelper = $this->questionHelper; $options = []; foreach ($servicesByProfile as $profile => $serviceNames) { $options[$profile] = sprintf('%s (for %s: %s)', $profile, count($serviceNames) === 1 ? 'service' : 'services', implode(', ', $serviceNames)); } - $profile = $questionHelper->choose($options, 'Enter a number to choose a container profile:'); + $profile = $this->questionHelper->choose($options, 'Enter a number to choose a container profile:'); } elseif (count($services) === 1) { $service = reset($services); $profile = $service->container_profile; diff --git a/src/Command/Route/RouteGetCommand.php b/src/Command/Route/RouteGetCommand.php index ede428c66..d50d63f40 100644 --- a/src/Command/Route/RouteGetCommand.php +++ b/src/Command/Route/RouteGetCommand.php @@ -47,7 +47,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $prefix = $this->config->getStr('service.env_prefix'); if (getenv($prefix . 'ROUTES') && !LocalHost::conflictsWithCommandLineOptions($input, $prefix)) { $this->io->debug('Reading routes from environment variable ' . $prefix . 'ROUTES'); - $decoded = json_decode(base64_decode(getenv($prefix . 'ROUTES'), true), true); + $decoded = json_decode((string) base64_decode(getenv($prefix . 'ROUTES'), true), true); if (empty($decoded)) { throw new \RuntimeException('Failed to decode: ' . $prefix . 'ROUTES'); } @@ -100,7 +100,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $questionHelper = $this->questionHelper; $items = []; $default = null; foreach ($routes as $route) { @@ -114,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $items[$originalUrl] .= ' - primary'; } } - $originalUrl = $questionHelper->choose($items, 'Enter a number to choose a route:', $default); + $originalUrl = $this->questionHelper->choose($items, 'Enter a number to choose a route:', $default); } if (!$selectedRoute && $originalUrl !== null && $originalUrl !== '') { diff --git a/src/Command/Route/RouteListCommand.php b/src/Command/Route/RouteListCommand.php index 5a0825db7..52fb9949b 100644 --- a/src/Command/Route/RouteListCommand.php +++ b/src/Command/Route/RouteListCommand.php @@ -52,7 +52,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $selection = null; if (getenv($prefix . 'ROUTES') && !LocalHost::conflictsWithCommandLineOptions($input, $prefix)) { $this->io->debug('Reading routes from environment variable ' . $prefix . 'ROUTES'); - $decoded = json_decode(base64_decode(getenv($prefix . 'ROUTES'), true), true); + $decoded = json_decode((string) base64_decode(getenv($prefix . 'ROUTES'), true), true); if (!is_array($decoded)) { throw new \RuntimeException('Failed to decode: ' . $prefix . 'ROUTES'); } diff --git a/src/Command/RuntimeOperation/ListCommand.php b/src/Command/RuntimeOperation/ListCommand.php index a39fe4e9b..c43ef8217 100644 --- a/src/Command/RuntimeOperation/ListCommand.php +++ b/src/Command/RuntimeOperation/ListCommand.php @@ -122,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function truncateCommand(string $cmd): string { - $lines = \preg_split('/\r?\n/', $cmd); + $lines = (array) \preg_split('/\r?\n/', $cmd); if (count($lines) > self::COMMAND_MAX_LENGTH) { return trim(implode("\n", array_slice($lines, 0, self::COMMAND_MAX_LENGTH))) . "\n# ..."; } diff --git a/src/Command/Self/SelfBuildCommand.php b/src/Command/Self/SelfBuildCommand.php index 0e5102f1f..003dfe981 100644 --- a/src/Command/Self/SelfBuildCommand.php +++ b/src/Command/Self/SelfBuildCommand.php @@ -130,7 +130,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Create a temporary box.json file for this build. - $originalConfig = json_decode(file_get_contents(CLI_ROOT . '/box.json'), true); + $originalConfig = json_decode((string) file_get_contents(CLI_ROOT . '/box.json'), true); $boxConfig = array_merge($originalConfig, $boxConfig); $boxConfig['base-path'] = CLI_ROOT; $tmpJson = tempnam(sys_get_temp_dir(), 'cli-box-'); @@ -138,7 +138,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $boxArgs[] = '--config=' . $tmpJson; $this->stdErr->writeln('Building Phar package using Box'); - $this->shell->execute($boxArgs, CLI_ROOT, true, false); + $this->shell->mustExecute($boxArgs, dir: CLI_ROOT, quiet: false); // Clean up the temporary file. if (!empty($tmpJson)) { @@ -157,7 +157,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln('The package was built successfully'); $output->writeln($phar); $this->stdErr->writeln([ - sprintf('Size: %s', FormatterHelper::formatMemory($size)), + sprintf('Size: %s', FormatterHelper::formatMemory((int) $size)), sprintf('SHA-1: %s', $sha1), sprintf('SHA-256: %s', $sha256), sprintf('Version: %s', $version), diff --git a/src/Command/Self/SelfInstallCommand.php b/src/Command/Self/SelfInstallCommand.php index 497994250..e99b42f3f 100644 --- a/src/Command/Self/SelfInstallCommand.php +++ b/src/Command/Self/SelfInstallCommand.php @@ -214,7 +214,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int '"$HOME/"' . escapeshellarg($rcDestination) ); - if (str_contains($currentShellConfig, $suggestedShellConfig)) { + if ($shellConfigFile !== false && str_contains($currentShellConfig, $suggestedShellConfig)) { $this->stdErr->writeln('Already configured: ' . $this->getShortPath($shellConfigFile) . ''); $this->stdErr->writeln(''); $this->markSelfInstalled($configDir); @@ -290,10 +290,13 @@ private function markSelfInstalled(string $configDir): void $filename = $configDir . DIRECTORY_SEPARATOR . self::INSTALLED_FILENAME; if (!file_exists($filename)) { $fs = new \Symfony\Component\Filesystem\Filesystem(); - $fs->dumpFile($filename, json_encode(['installed_at' => date('c')])); + $fs->dumpFile($filename, (string) json_encode(['installed_at' => date('c')])); } } + /** + * @return string[] + */ private function getRunAdvice(string $shellConfigFile, string $binDir, ?bool $inPath = null, bool $newTerminal = false): array { $advice = [ diff --git a/src/Command/Self/SelfReleaseCommand.php b/src/Command/Self/SelfReleaseCommand.php index 35f8c3f0a..c5a46c262 100644 --- a/src/Command/Self/SelfReleaseCommand.php +++ b/src/Command/Self/SelfReleaseCommand.php @@ -100,8 +100,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln('Last version number: ' . $lastVersion . ''); } else { - $lastTag = $this->shell->execute(['git', 'describe', '--tags', '--abbrev=0'], CLI_ROOT, true); - $lastVersion = ltrim((string) $lastTag, 'v'); + $lastTag = $this->shell->mustExecute(['git', 'describe', '--tags', '--abbrev=0'], dir: CLI_ROOT); + $lastVersion = ltrim($lastTag, 'v'); $this->stdErr->writeln('Last version number (from latest Git tag): ' . $lastVersion . ''); } @@ -263,7 +263,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $pharFilename, '--version' ], null, true); - if (!str_contains($versionInPhar, (string) $newVersion)) { + if (!str_contains((string) $versionInPhar, (string) $newVersion)) { $this->stdErr->writeln('The file ' . $pharFilename . ' reports a different version: "' . $versionInPhar . '"'); return 1; @@ -354,9 +354,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int ], 'debug' => $output->isDebug(), ]); - $release = Utils::jsonDecode((string) $response->getBody(), true); + $release = (array) Utils::jsonDecode((string) $response->getBody(), true); $releaseUrl = $repoApiUrl . '/releases/' . $release['id']; - $uploadUrl = preg_replace('/\{.+?\}/', '', (string) $release['upload_url']); + $uploadUrl = preg_replace('/\{.+?}/', '', (string) $release['upload_url']); // Upload the Phar to the GitHub release. $this->stdErr->writeln('Uploading the Phar file to the release'); @@ -479,7 +479,7 @@ private function getGitChangelog(string $since): string return ''; } - $changelog = preg_replace('/^[^\*\n]/m', ' $0', $changelog); + $changelog = preg_replace('/^[^*\n]/m', ' $0', $changelog); $changelog = preg_replace('/\n+\*/', "\n*", (string) $changelog); $changelog = trim((string) $changelog); diff --git a/src/Command/Self/SelfStatsCommand.php b/src/Command/Self/SelfStatsCommand.php index 67aeccc91..2dcaa8c4b 100644 --- a/src/Command/Self/SelfStatsCommand.php +++ b/src/Command/Self/SelfStatsCommand.php @@ -53,7 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'per_page' => (int) $input->getOption('count'), ], ]); - $releases = Utils::jsonDecode((string) $response->getBody(), true); + $releases = (array) Utils::jsonDecode((string) $response->getBody(), true); if (empty($releases)) { $this->stdErr->writeln('No releases found.'); diff --git a/src/Command/Server/ServerCommandBase.php b/src/Command/Server/ServerCommandBase.php index 45ef65212..cc725d0c3 100644 --- a/src/Command/Server/ServerCommandBase.php +++ b/src/Command/Server/ServerCommandBase.php @@ -1,4 +1,6 @@ >|null */ private ?array $serverInfo = null; #[Required] @@ -39,6 +42,8 @@ public function isEnabled(): bool /** * Checks whether another server is running for an app. + * + * @return array|false */ protected function isServerRunningForApp(string $appId, string $projectRoot): array|false { @@ -75,7 +80,7 @@ protected function isServerRunningForAddress(string $address): bool|int $pidFile = $this->getPidFile($address); $serverInfo = $this->getServerInfo(); if (file_exists($pidFile)) { - $pid = file_get_contents($pidFile); + $pid = (int) file_get_contents($pidFile); } elseif (isset($serverInfo[$address])) { $pid = $serverInfo[$address]['pid']; } @@ -95,6 +100,17 @@ protected function isServerRunningForAddress(string $address): bool|int /** * Gets info on currently running servers. + * + * @return array */ protected function getServerInfo(bool $running = true): array { @@ -103,7 +119,7 @@ protected function getServerInfo(bool $running = true): array // @todo move this to State service (in a new major version) $filename = $this->config->getWritableUserDir() . '/local-servers.json'; if (file_exists($filename)) { - $this->serverInfo = (array) json_decode(file_get_contents($filename), true); + $this->serverInfo = (array) json_decode((string) file_get_contents($filename), true); } } @@ -159,14 +175,17 @@ protected function stopServer(string $address, ?int $pid = null): bool return $success; } - protected function writeServerInfo(string $address, int $pid, array $info = []): void + /** + * @param array{appId: string, projectRoot: string, logFile: string, docRoot: string} $info + */ + protected function writeServerInfo(string $address, int $pid, array $info): void { file_put_contents($this->getPidFile($address), $pid); list($ip, $port) = explode(':', $address); $this->serverInfo[$address] = $info + [ 'address' => $address, 'pid' => $pid, - 'port' => $port, + 'port' => (int) $port, 'ip' => $ip, ]; $this->saveServerInfo(); @@ -203,8 +222,8 @@ protected function getPidFile(string $address): string * @param string $address * @param string $docRoot * @param string $projectRoot - * @param array $appConfig - * @param array $env + * @param array $appConfig + * @param array $env * * @return Process *@throws \Exception @@ -268,7 +287,7 @@ protected function createServerProcess(string $address, string $docRoot, string /** * Get custom PHP configuration for the built-in web server. * - * @return array + * @return array */ private function getServerPhpConfig(): array { @@ -276,7 +295,7 @@ private function getServerPhpConfig(): array // Ensure $_ENV is populated. $variables_order = ini_get('variables_order'); - if (!str_contains($variables_order, 'E')) { + if (!str_contains((string) $variables_order, 'E')) { $phpConfig['variables_order'] = 'E' . $variables_order; } @@ -321,6 +340,9 @@ protected function openLog(string $logFile): false|OutputInterface return false; } + /** + * @return array> + */ private function getRoutesList(string $projectRoot, string $address): array { $localProject = $this->localProject; @@ -345,18 +367,25 @@ private function getRoutesList(string $projectRoot, string $address): array /** * Creates the virtual environment variables for a local server. + * + * @param array $appConfig + * + * @return array */ protected function createEnv(string $projectRoot, string $docRoot, string $address, array $appConfig): array { $realDocRoot = realpath($docRoot); + if (!$realDocRoot) { + throw new \RuntimeException('Failed to resolve directory: ' . $docRoot); + } $envPrefix = $this->config->getStr('service.env_prefix'); $env = [ '_PLATFORM_VARIABLES_PREFIX' => $envPrefix, $envPrefix . 'ENVIRONMENT' => '_local', - $envPrefix . 'APPLICATION' => base64_encode(json_encode($appConfig)), + $envPrefix . 'APPLICATION' => base64_encode((string) json_encode($appConfig)), $envPrefix . 'APPLICATION_NAME' => $appConfig['name'] ?? '', $envPrefix . 'DOCUMENT_ROOT' => $realDocRoot, - $envPrefix . 'ROUTES' => base64_encode(json_encode($this->getRoutesList($projectRoot, $address))), + $envPrefix . 'ROUTES' => base64_encode((string) json_encode($this->getRoutesList($projectRoot, $address))), ]; list($env['IP'], $env['PORT']) = explode(':', $address); diff --git a/src/Command/Service/MongoDB/MongoDumpCommand.php b/src/Command/Service/MongoDB/MongoDumpCommand.php index aa79fda21..0015c55c3 100644 --- a/src/Command/Service/MongoDB/MongoDumpCommand.php +++ b/src/Command/Service/MongoDB/MongoDumpCommand.php @@ -48,13 +48,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $envPrefix = $this->config->getStr('service.env_prefix'); $selection = $this->selector->getSelection($input, new SelectorConfig( - allowLocalHost: getenv($envPrefix . 'RELATIONSHIPS') !== false, + allowLocalHost: getenv($envPrefix . 'RELATIONSHIPS') !== false + && getenv($envPrefix . 'APPLICATION_NAME') !== false, )); $host = $this->selector->getHostFromSelection($input, $selection); if ($host instanceof RemoteHost) { $appName = $selection->getAppName(); } else { - $appName = getenv($envPrefix . 'APPLICATION_NAME'); + $appName = (string) getenv($envPrefix . 'APPLICATION_NAME'); } $dumpFile = false; @@ -66,8 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($dumpFile) { if (file_exists($dumpFile)) { - $questionHelper = $this->questionHelper; - if (!$questionHelper->confirm("File exists: $dumpFile. Overwrite?")) { + if (!$this->questionHelper->confirm("File exists: $dumpFile. Overwrite?")) { return 1; } } diff --git a/src/Command/Service/MongoDB/MongoExportCommand.php b/src/Command/Service/MongoDB/MongoExportCommand.php index 244354e92..b74cd6bce 100644 --- a/src/Command/Service/MongoDB/MongoExportCommand.php +++ b/src/Command/Service/MongoDB/MongoExportCommand.php @@ -67,8 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (empty($collections)) { throw new InvalidArgumentException('No collections found. You can specify one with the --collection (-c) option.'); } - $questionHelper = $this->questionHelper; - $collection = $questionHelper->choose(array_combine($collections, $collections), 'Enter a number to choose a collection:', null, false); + $collection = $this->questionHelper->choose(array_combine($collections, $collections), 'Enter a number to choose a collection:', null, false); } $command = 'mongoexport ' . $this->relationships->getDbCommandArgs('mongoexport', $service); @@ -99,10 +98,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Get collections in the MongoDB database. * - * @param array $service + * @param array{username: string, password: string, host: string, port:int, path: string} $service * @param HostInterface $host * - * @return array + * @return string[] */ private function getCollections(array $service, HostInterface $host): array { @@ -127,6 +126,6 @@ private function getCollections(array $service, HostInterface $host): array $collections = json_decode($result, true) ?: []; - return array_filter($collections, fn($collection): bool => !str_starts_with((string) $collection, 'system.')); + return array_filter($collections, fn(string $collection): bool => !str_starts_with((string) $collection, 'system.')); } } diff --git a/src/Command/Session/SessionSwitchCommand.php b/src/Command/Session/SessionSwitchCommand.php index d71ab1bb6..a11a3baec 100644 --- a/src/Command/Session/SessionSwitchCommand.php +++ b/src/Command/Session/SessionSwitchCommand.php @@ -50,13 +50,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln('The new session ID is required'); return 1; } - $questionHelper = $this->questionHelper; $autocomplete = \array_merge(['default' => ''], \array_flip($this->api->listSessionIds())); unset($autocomplete[$previousId]); $default = count($autocomplete) === 1 ? key($autocomplete) : false; $this->stdErr->writeln('The current session ID is: ' . $previousId . ''); $this->stdErr->writeln(''); - $newId = $questionHelper->askInput('Enter a new session ID', $default ?: null, \array_keys($autocomplete), function ($sessionId) { + $newId = $this->questionHelper->askInput('Enter a new session ID', $default ?: null, \array_keys($autocomplete), function ($sessionId) { if (empty($sessionId)) { throw new \RuntimeException('The session ID cannot be empty'); } diff --git a/src/Command/SourceOperation/ListCommand.php b/src/Command/SourceOperation/ListCommand.php index 2300c8d68..1c94ba4fe 100644 --- a/src/Command/SourceOperation/ListCommand.php +++ b/src/Command/SourceOperation/ListCommand.php @@ -82,12 +82,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function truncateCommand($cmd): string + private function truncateCommand(string $cmd): string { - $lines = \preg_split('/\r?\n/', (string) $cmd); + $lines = (array) \preg_split('/\r?\n/', $cmd); if (count($lines) > self::COMMAND_MAX_LENGTH) { return trim(implode("\n", array_slice($lines, 0, self::COMMAND_MAX_LENGTH))) . "\n# ..."; } - return trim((string) $cmd); + return trim($cmd); } } diff --git a/src/Command/SourceOperation/RunCommand.php b/src/Command/SourceOperation/RunCommand.php index 72607c18b..f7f218d0d 100644 --- a/src/Command/SourceOperation/RunCommand.php +++ b/src/Command/SourceOperation/RunCommand.php @@ -59,13 +59,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln('The operation argument is required in non-interactive mode.'); return 1; } - $questionHelper = $this->questionHelper; $choices = []; foreach ($sourceOps as $sourceOp) { $choices[$sourceOp->operation] = $sourceOp->operation . ' (app: ' . $sourceOp->app . ')'; } ksort($choices, SORT_NATURAL); - $operation = $questionHelper->choose($choices, 'Enter a number to choose an operation to run:', null, false); + $operation = $this->questionHelper->choose($choices, 'Enter a number to choose an operation to run:', null, false); } $operationNames = []; @@ -105,9 +104,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param array $variables + * @param string[] $variables * - * @return array + * @return array> */ private function parseVariables(array $variables): array { diff --git a/src/Command/SshKey/SshKeyAddCommand.php b/src/Command/SshKey/SshKeyAddCommand.php index b70ace914..f4e88e54e 100644 --- a/src/Command/SshKey/SshKeyAddCommand.php +++ b/src/Command/SshKey/SshKeyAddCommand.php @@ -66,11 +66,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ($this->shell->commandExists('ssh-keygen') && $this->questionHelper->confirm('Generate a new key?')) { // Offer to generate a key. - $newKeyPath = $this->askNewKeyPath($this->questionHelper); + $newKeyPath = $this->askNewKeyPath(); $this->stdErr->writeln(''); $args = ['ssh-keygen', '-t', 'ed25519', '-f', $newKeyPath, '-N', '']; - $this->shell->execute($args, null, true); + $this->shell->mustExecute($args); $publicKeyPath = $newKeyPath . '.pub'; $this->stdErr->writeln("Generated a new key: $publicKeyPath\n"); @@ -166,13 +166,9 @@ protected function keyExistsByFingerprint(string $fingerprint): bool } /** - * Find the default path for a new SSH key. - * - * @param QuestionHelper $questionHelper - * - * @return string + * Finds the default path for a new SSH key. */ - private function askNewKeyPath(QuestionHelper $questionHelper): string + private function askNewKeyPath(): string { $basename = 'id_ed25519-' . $this->config->getStr('application.slug') . '-' . $this->api->getMyAccount()['username']; $sshDir = $this->config->getHomeDirectory() . DIRECTORY_SEPARATOR . '.ssh'; diff --git a/src/Command/SshKey/SshKeyDeleteCommand.php b/src/Command/SshKey/SshKeyDeleteCommand.php index c3e8850fa..26c076fc0 100644 --- a/src/Command/SshKey/SshKeyDeleteCommand.php +++ b/src/Command/SshKey/SshKeyDeleteCommand.php @@ -45,8 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($keys as $key) { $options[$key->key_id] = sprintf('%s (%s)', $key->key_id, $key->title ?: $key->fingerprint); } - $questionHelper = $this->questionHelper; - $id = $questionHelper->choose($options, 'Enter a number to choose a key to delete:', null, false); + $id = $this->questionHelper->choose($options, 'Enter a number to choose a key to delete:', null, false); } if (empty($id) || !is_numeric($id)) { $this->stdErr->writeln('You must specify the ID of the SSH key to delete.'); @@ -59,7 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $key = $this->api->getClient() - ->getSshKey($id); + ->getSshKey((string) $id); if (!$key) { $this->stdErr->writeln("SSH key not found: $id"); diff --git a/src/Command/SubscriptionInfoCommand.php b/src/Command/SubscriptionInfoCommand.php index 9cfc5c83f..a283da0e4 100644 --- a/src/Command/SubscriptionInfoCommand.php +++ b/src/Command/SubscriptionInfoCommand.php @@ -46,7 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (empty($id)) { $selection = $this->selector->getSelection($input); $project = $selection->getProject(); - $id = $project->getSubscriptionId(); + $id = (string) $project->getSubscriptionId(); } $subscription = $this->api->loadSubscription($id, $project, $input->getArgument('value') !== null); diff --git a/src/Command/Team/Project/TeamProjectAddCommand.php b/src/Command/Team/Project/TeamProjectAddCommand.php index 812ba3c95..7a31edc03 100644 --- a/src/Command/Team/Project/TeamProjectAddCommand.php +++ b/src/Command/Team/Project/TeamProjectAddCommand.php @@ -197,6 +197,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Displays a list of projects. + * + * @param string[] $projectIds */ private function displayProjectsAsList(array $projectIds, OutputInterface $output): void { diff --git a/src/Command/Team/TeamCommandBase.php b/src/Command/Team/TeamCommandBase.php index 4f18b2dec..5f9159803 100644 --- a/src/Command/Team/TeamCommandBase.php +++ b/src/Command/Team/TeamCommandBase.php @@ -153,7 +153,7 @@ protected function selectOrganization(InputInterface $input): Organization|false * * @param Organization $organization The organization. * @param bool $fetchAllPages If false, only one page will be fetched. - * @param array $params Extra query parameters. + * @param array $params Extra query parameters. * * @return Team[] */ diff --git a/src/Command/Team/TeamCreateCommand.php b/src/Command/Team/TeamCreateCommand.php index 918c55b67..4c163795b 100644 --- a/src/Command/Team/TeamCreateCommand.php +++ b/src/Command/Team/TeamCreateCommand.php @@ -109,11 +109,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $roles; }; + /** @var string[] $projectPermissions */ + $projectPermissions = []; if ($roleInput = ArrayArgument::getOption($input, 'role')) { - $specifiedProjectRole = $this->getSpecifiedProjectRole($roleInput); - $specifiedTypeRoles = $this->getSpecifiedTypeRoles($roleInput); - $projectPermissions = [$specifiedProjectRole]; - foreach ($specifiedTypeRoles as $type => $role) { + if ($specifiedProjectRole = $this->getSpecifiedProjectRole($roleInput)) { + $projectPermissions[] = [$specifiedProjectRole]; + } + foreach ($this->getSpecifiedTypeRoles($roleInput) as $type => $role) { if ($role !== 'none') { $projectPermissions[] = $type . ':' . $role; } @@ -121,15 +123,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ($input->isInteractive()) { $projectRole = $this->showProjectRoleForm($update ? $getProjectRole($existingTeam->project_permissions) : 'viewer', $input); $this->stdErr->writeln(''); - $environmentTypeRoles = []; + + $projectPermissions[] = $projectRole; + if ($projectRole !== 'admin') { $environmentTypeRoles = $this->showTypeRolesForm($update ? $getEnvTypeRoles($existingTeam->project_permissions) : [], $input); $this->stdErr->writeln(''); - } - $projectPermissions = [$projectRole]; - foreach ($environmentTypeRoles as $type => $role) { - if ($role !== 'none') { - $projectPermissions[] = $type . ':' . $role; + foreach ($environmentTypeRoles as $type => $role) { + if ($role !== 'none') { + $projectPermissions[] = $type . ':' . $role; + } } } } else { @@ -289,10 +292,10 @@ private function matchRole(string $input, array $roles): string /** * Show the form for entering environment type roles. * - * @param array $defaultTypeRoles + * @param array $defaultTypeRoles * @param InputInterface $input * - * @return array + * @return array * The environment type roles (keyed by type ID) including the user's * answers. */ @@ -337,16 +340,15 @@ private function validateEnvironmentTypeRole(string $value): string /** * Extract the specified project role from the list (given in --role). * - * @param array &$roles + * @param string[] $roles * * @return string|null * The project role, or null if none is specified. */ - private function getSpecifiedProjectRole(array &$roles): ?string + private function getSpecifiedProjectRole(array $roles): ?string { - foreach ($roles as $key => $role) { - if (!str_contains((string) $role, ':')) { - unset($roles[$key]); + foreach ($roles as $role) { + if (!str_contains($role, ':')) { return $this->validateProjectRole($role); } } diff --git a/src/Command/Tunnel/TunnelOpenCommand.php b/src/Command/Tunnel/TunnelOpenCommand.php index 018a308f0..012466b76 100644 --- a/src/Command/Tunnel/TunnelOpenCommand.php +++ b/src/Command/Tunnel/TunnelOpenCommand.php @@ -87,9 +87,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($environment->is_main) { - $questionHelper = $this->questionHelper; $confirmText = \sprintf('Are you sure you want to open SSH tunnel(s) to the environment %s?', $this->api->getEnvironmentLabel($environment, 'comment')); - if (!$questionHelper->confirm($confirmText)) { + if (!$this->questionHelper->confirm($confirmText)) { return 1; } $this->stdErr->writeln(''); diff --git a/src/Command/Tunnel/TunnelSingleCommand.php b/src/Command/Tunnel/TunnelSingleCommand.php index 7124b748c..73cc5d32c 100644 --- a/src/Command/Tunnel/TunnelSingleCommand.php +++ b/src/Command/Tunnel/TunnelSingleCommand.php @@ -60,7 +60,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($environment->is_main) { - $questionHelper = $this->questionHelper; $confirmText = sprintf( 'Are you sure you want to open an SSH tunnel to' . ' the relationship %s on the' @@ -68,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $service['_relationship_name'], $this->api->getEnvironmentLabel($environment, false) ); - if (!$questionHelper->confirm($confirmText)) { + if (!$this->questionHelper->confirm($confirmText)) { return 1; } $this->stdErr->writeln(''); diff --git a/src/Command/User/UserAddCommand.php b/src/Command/User/UserAddCommand.php index 105ce799a..44b283f5b 100644 --- a/src/Command/User/UserAddCommand.php +++ b/src/Command/User/UserAddCommand.php @@ -556,7 +556,7 @@ private function showProjectRoleForm(string $defaultRole, InputInterface $input) * @param ProjectAccess $projectAccess * @param EnvironmentType[] $environmentTypes * - * @return array + * @return array */ private function getTypeRoles(ProjectAccess $projectAccess, array $environmentTypes): array { @@ -588,11 +588,11 @@ private function getTypeRoles(ProjectAccess $projectAccess, array $environmentTy /** * Show the form for entering environment type roles. * - * @param array $defaultTypeRoles + * @param array $defaultTypeRoles * @param EnvironmentType[] $environmentTypes * @param InputInterface $input * - * @return array + * @return array * The environment type roles (keyed by type ID) including the user's * answers. */ @@ -633,16 +633,15 @@ private function showTypeRolesForm(array $defaultTypeRoles, array $environmentTy /** * Extract the specified project role from the list (given in --role). * - * @param array &$roles + * @param string[] $roles * * @return string|null * The project role, or null if none is specified. */ - private function getSpecifiedProjectRole(array &$roles): ?string + private function getSpecifiedProjectRole(array $roles): ?string { - foreach ($roles as $key => $role) { - if (!str_contains((string) $role, ':')) { - unset($roles[$key]); + foreach ($roles as $role) { + if (!str_contains($role, ':')) { return $this->validateProjectRole($role); } } diff --git a/src/Command/User/UserListCommand.php b/src/Command/User/UserListCommand.php index e0e466942..9dda82512 100644 --- a/src/Command/User/UserListCommand.php +++ b/src/Command/User/UserListCommand.php @@ -119,7 +119,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln("To change a user's role(s), run: $executable user:update"); if ($this->accessApi->centralizedPermissionsEnabled() && $this->config->get('api.teams')) { $organization = $this->api->getOrganizationById($project->getProperty('organization')); - if (in_array('teams', $organization->capabilities) && $organization->hasLink('members')) { + if ($organization && in_array('teams', $organization->capabilities) && $organization->hasLink('members')) { $this->stdErr->writeln(''); $this->stdErr->writeln(sprintf("To list teams with access to the project, run: $executable teams -p %s", $project->id)); } diff --git a/src/Console/ArrayArgument.php b/src/Console/ArrayArgument.php index 274c7d08d..959b03c5f 100644 --- a/src/Console/ArrayArgument.php +++ b/src/Console/ArrayArgument.php @@ -47,13 +47,17 @@ public static function getOption(InputInterface $input, string $optionName): arr * * @param string[] $args * - * @return array + * @return string[] */ public static function split(array $args): array { $split = []; foreach ($args as $arg) { - $split = \array_merge($split, \preg_split('/[,\s]+/', $arg)); + $splitArg = \preg_split('/[,\s]+/', $arg); + if (!$splitArg) { + throw new \RuntimeException('Failed to split argument by commas/whitespace'); + } + $split = \array_merge($split, $splitArg); } return \array_filter($split, fn(string $a): bool => \strlen($a) > 0); } diff --git a/src/CredentialHelper/Manager.php b/src/CredentialHelper/Manager.php index a3846d456..40ea9a991 100644 --- a/src/CredentialHelper/Manager.php +++ b/src/CredentialHelper/Manager.php @@ -215,13 +215,13 @@ private function extractBinFromArchive(string $archiveContents, bool $zip, strin } } elseif ($this->shell->commandExists('unzip')) { $command = 'unzip ' . escapeshellarg($tmpFile) . ' -d ' . escapeshellarg($tmpDir); - $this->shell->execute($command, null, true); + $this->shell->mustExecute($command); } else { throw new \RuntimeException('Failed to extract zip: unzip is not installed'); } } else { $command = 'tar -xzp -f ' . escapeshellarg($tmpFile) . ' -C ' . escapeshellarg($tmpDir); - $this->shell->execute($command, null, true); + $this->shell->mustExecute($command); } if (!file_exists($tmpDir . DIRECTORY_SEPARATOR . $internalFilename)) { throw new \RuntimeException('File not found: ' . $tmpDir . DIRECTORY_SEPARATOR . $internalFilename); diff --git a/src/CredentialHelper/SessionStorage.php b/src/CredentialHelper/SessionStorage.php index 34556bcd8..074c94e3b 100644 --- a/src/CredentialHelper/SessionStorage.php +++ b/src/CredentialHelper/SessionStorage.php @@ -139,7 +139,7 @@ private function serialize(array $data): string */ private function deserialize(string $data): array { - $result = json_decode(base64_decode($data, true), true); + $result = json_decode((string) base64_decode($data, true), true); return is_array($result) ? $result : []; } diff --git a/src/Local/LocalProject.php b/src/Local/LocalProject.php index 34f1f690b..44b4e00af 100644 --- a/src/Local/LocalProject.php +++ b/src/Local/LocalProject.php @@ -55,7 +55,7 @@ public function readProjectConfigFile(string $dir, string $configFile): ?array public function parseGitUrl(string $gitUrl): false|array { $gitDomain = $this->config->getStr('detection.git_domain'); - $pattern = '/^([a-z0-9]{12,})@git\.(([a-z0-9\-]+\.)?' . preg_quote((string) $gitDomain) . '):\1\.git$/'; + $pattern = '/^([a-z0-9]{12,})@git\.(([a-z0-9\-]+\.)?' . preg_quote($gitDomain) . '):\1\.git$/'; if (!preg_match($pattern, $gitUrl, $matches)) { return false; } diff --git a/src/Model/EnvironmentDomain.php b/src/Model/EnvironmentDomain.php index c5cc0c1ae..b2d98586c 100644 --- a/src/Model/EnvironmentDomain.php +++ b/src/Model/EnvironmentDomain.php @@ -16,6 +16,7 @@ * @property-read string $replacement_for * @property-read string $created_at * @property-read string $updated_at + * @property-read array $ssl */ class EnvironmentDomain extends ApiResourceBase { diff --git a/src/Model/Metrics/Sketch.php b/src/Model/Metrics/Sketch.php index 3a38c538d..0b92b6aae 100644 --- a/src/Model/Metrics/Sketch.php +++ b/src/Model/Metrics/Sketch.php @@ -8,6 +8,10 @@ private function __construct(private string|int|float|null $sum, private string| { } + /** + * @param array{value: array, info: array} $value + * @return self + */ public static function fromApiValue(array $value): self { return new Sketch( @@ -32,6 +36,12 @@ public function average(): float if ($this->isInfinite()) { throw new \RuntimeException('Cannot find the average of an infinite value'); } + if ($this->sum === null) { + return 0; + } + if (is_string($this->sum)) { + throw new \RuntimeException('Cannot find the average of a string "sum": ' . $this->sum); + } return $this->sum / (float) $this->count; } } diff --git a/src/Service/Api.php b/src/Service/Api.php index 5b3d86c17..a4c0cd4cb 100644 --- a/src/Service/Api.php +++ b/src/Service/Api.php @@ -96,6 +96,8 @@ class Api /** * A cache of not-found environment IDs. * + * @var array + * * @see Api::getEnvironment() */ private static array $notFound = []; @@ -191,9 +193,11 @@ public function listSessionIds(): array } $dir = $this->config->getSessionDir(); $files = glob($dir . '/sess-cli-*', GLOB_NOSORT); - foreach ($files as $file) { - if (\preg_match('@/sess-cli-([a-z0-9_-]+)@i', $file, $matches)) { - $ids[] = $matches[1]; + if ($files !== false) { + foreach ($files as $file) { + if (\preg_match('@/sess-cli-([a-z0-9_-]+)@i', $file, $matches)) { + $ids[] = $matches[1]; + } } } $ids = \array_filter($ids, fn($id): bool => !str_starts_with((string) $id, 'api-token-')); @@ -261,7 +265,7 @@ public function deleteAllSessions(): void * * @see Connector::__construct() * - * @return array + * @return array */ private function getConnectorOptions(): array { $connectorOptions = []; @@ -360,7 +364,7 @@ private function onStepUpAuthResponse(ResponseInterface $response): ?AccessToken $session = $this->getClient(false)->getConnector()->getSession(); $previousAccessToken = $session->get('accessToken'); - $body = Utils::jsonDecode((string) $response->getBody(), true); + $body = (array) Utils::jsonDecode((string) $response->getBody(), true); $authMethods = $body['amr'] ?? []; $maxAge = $body['max_age'] ?? null; @@ -415,6 +419,8 @@ private function onRefreshError(IdentityProviderException $e): ?AccessToken /** * Tests if an HTTP response from refreshing a token indicates that the user's SSO session has expired. + * + * @param array $data */ private function isSsoSessionExpired(array $data): bool { @@ -427,6 +433,8 @@ private function isSsoSessionExpired(array $data): bool /** * Tests if an error from refreshing a token indicates that the user's API token is invalid. + * + * @param array $body */ private function isApiTokenInvalid(mixed $body): bool { @@ -470,7 +478,7 @@ private function tokenFromSession(SessionInterface $session): ?AccessToken { * * @see Client::__construct() * - * @return array + * @return array */ public function getGuzzleOptions(): array { $options = [ @@ -906,7 +914,7 @@ public function getMyAccount(bool $reset = false): array * * @param bool $reset * - * @return array{'id': string, 'username': string, 'mail': string, 'display_name': string, 'ssh_keys': array} + * @return array{'id': string, 'username': string, 'mail': string, 'display_name': string, 'ssh_keys': array} */ private function getLegacyAccountInfo(bool $reset = false): array { @@ -924,11 +932,14 @@ private function getLegacyAccountInfo(bool $reset = false): array /** * Shortcut to return the ID of the current user. - * */ - public function getMyUserId(bool $reset = false): string|false + public function getMyUserId(bool $reset = false): string { - return $this->getClient()->getMyUserId($reset); + $id = $this->getClient()->getMyUserId($reset); + if (!$id) { + throw new \RuntimeException('No user ID found for the current session.'); + } + return $id; } /** @@ -1468,7 +1479,7 @@ public function checkUserVerification(): array // Check the API to see if verification is required. $request = new Request('POST', '/me/verification'); $response = $this->getHttpClient()->send($request); - return Utils::jsonDecode((string) $response->getBody(), true); + return (array) Utils::jsonDecode((string) $response->getBody(), true); } /** @@ -1480,7 +1491,7 @@ public function checkCanCreate(Organization $org): array { $request = new Request('GET', $org->getUri() . '/subscriptions/can-create'); $response = $this->getHttpClient()->send($request); - return Utils::jsonDecode((string) $response->getBody(), true); + return (array) Utils::jsonDecode((string) $response->getBody(), true); } /** @@ -1558,7 +1569,7 @@ public function getConsoleURL(Project $project, bool $reset = false): string|fal return ltrim($this->config->getStr('service.console_url'), '/') . '/' . rawurlencode((string) $firstSegment) . '/' . rawurlencode($project->id); } - $subscription = $this->loadSubscription($project->getSubscriptionId(), $project); + $subscription = $this->loadSubscription((string) $project->getSubscriptionId(), $project); return $subscription ? $subscription->project_ui : false; } @@ -1633,7 +1644,7 @@ public function supportsSizingApi(Project $project, ?EnvironmentDeployment $depl } $request = new Request('GET', $project->getUri() . '/settings'); $response = $this->getHttpClient()->send($request); - $settings = Utils::jsonDecode((string) $response->getBody(), true); + $settings = (array) Utils::jsonDecode((string) $response->getBody(), true); $this->cache->save($cacheKey, $settings, (int) $this->config->get('api.projects_ttl')); return !empty($settings['sizing_api_enabled']); } diff --git a/src/Service/QuestionHelper.php b/src/Service/QuestionHelper.php index e7b288cda..2489f4fcb 100644 --- a/src/Service/QuestionHelper.php +++ b/src/Service/QuestionHelper.php @@ -75,18 +75,18 @@ public function confirm(string $questionText, bool $default = true): bool /** * Provides an interactive choice question. * - * @param array $items An associative array of choices. + * @param array $items An associative array of choices. * @param string $text Some text to precede the choices. - * @param mixed $default A default (as a key in $items). + * @param string|null $default A default (as a key in $items). * @param bool $skipOnOne Whether to skip the choice if there is only one * item. * - * @return int|string|null + * @return string * The chosen item (as a key in $items). * * @throws \RuntimeException on failure */ - public function choose(array $items, string $text = 'Enter a number to choose an item:', mixed $default = null, bool $skipOnOne = true): int|string|null + public function choose(array $items, string $text = 'Enter a number to choose an item:', ?string $default = null, bool $skipOnOne = true): string { if (count($items) === 1) { if ($skipOnOne) { @@ -108,7 +108,7 @@ public function choose(array $items, string $text = 'Enter a number to choose an if (!$this->input->isInteractive()) { if (!isset($defaultKey)) { - return null; + throw new \RuntimeException('A choice is needed, input is not interactive, and no default is available.'); } $choice = $itemList[$defaultKey]; $choiceKey = array_search($choice, $items, true); diff --git a/src/Service/Relationships.php b/src/Service/Relationships.php index ea1428500..4bee1ef9f 100644 --- a/src/Service/Relationships.php +++ b/src/Service/Relationships.php @@ -52,7 +52,16 @@ public function chooseDatabase(HostInterface $host, InputInterface $input, Outpu * @param OutputInterface $output * @param string[] $schemes Filter by scheme. * - * @return array|false + * @return false|array{ + * scheme: string, + * username: string, + * password: string, + * host: string, + * port:int, + * path: string, + * _relationship_name: string, + * _relationship_key: string, + * } */ public function chooseService(HostInterface $host, InputInterface $input, OutputInterface $output, array $schemes = []): array|false { @@ -247,16 +256,14 @@ public function mariaDbCommandWithFallback(string $cmd): string /** * Returns command-line arguments to connect to a database. * - * @param string $command The command that will need arguments - * (one of 'psql', 'pg_dump', 'mysql', - * 'mysqldump', 'mariadb' or - * 'mariadb-dump'). - * @param array $database The database definition from the - * relationship. - * @param string|null $schema The name of a database schema, or - * null to use the default schema, or - * an empty string to not select a - * schema. + * @param string $command + * The command that will need arguments (one of 'psql', 'pg_dump', + * 'mysql', mysqldump', 'mariadb' or 'mariadb-dump'). + * @param array{username: string, password: string, host: string, port:int, path: string} $database + * The database definition from the relationship. + * @param string|null $schema + * The name of a database schema, null to use the default schema, or an + * empty string to not select a schema. * * @return string * The command line arguments (excluding the $command). diff --git a/src/Service/RemoteEnvVars.php b/src/Service/RemoteEnvVars.php index 929b8a60e..8bc6fcd66 100644 --- a/src/Service/RemoteEnvVars.php +++ b/src/Service/RemoteEnvVars.php @@ -79,6 +79,6 @@ public function getArrayEnvVar(string $variable, HostInterface $host, bool $refr { $value = $this->getEnvVar($variable, $host, $refresh); - return json_decode(base64_decode($value), true) ?: []; + return json_decode((string) base64_decode($value), true) ?: []; } } diff --git a/src/Service/Rsync.php b/src/Service/Rsync.php index aac185dab..01f74c248 100644 --- a/src/Service/Rsync.php +++ b/src/Service/Rsync.php @@ -120,6 +120,6 @@ private function doSync(string $from, string $to, string $sshUrl, array $options } } - $this->shell->execute($params, null, true, false, $this->env($sshUrl), null); + $this->shell->mustExecute($params, quiet: false, env: $this->env($sshUrl), timeout: null); } } diff --git a/src/Service/Shell.php b/src/Service/Shell.php index f19cae0f2..7f800ed94 100644 --- a/src/Service/Shell.php +++ b/src/Service/Shell.php @@ -77,18 +77,22 @@ public function executeSimple(string $commandline, ?string $dir = null, array $e * @param string|null $input * * @return bool|string - * False if the command fails, true if it succeeds with no output, or a - * string if it succeeds with output. - *@throws RuntimeException - * If $mustRun is enabled and the command fails. + * False if the command fails and $mustRun is false, true if it succeeds + * with no output, or a string if it succeeds with output. * + * @throws RuntimeException + * If $mustRun is enabled and the command fails. */ public function execute(array|string $args, ?string $dir = null, bool $mustRun = false, bool $quiet = true, array $env = [], ?int $timeout = 3600, mixed $input = null): bool|string { $process = $this->setupProcess($args, $dir, $env, $timeout, $input); - $result = $this->runProcess($process, $mustRun, $quiet); + $exitCode = $this->runProcess($process, $mustRun, $quiet); + if ($exitCode > 0) { + return false; + } + $output = $process->getOutput(); - return is_int($result) ? $result === 0 : $result; + return $output ? rtrim($output) : true; } /** @@ -101,6 +105,16 @@ public function executeCaptureProcess(string|array $args, ?string $dir = null, b return $process; } + /** + * Executes a command and returns its raw output, throwing an exception on failure. + */ + public function mustExecute(string|array $args, ?string $dir = null, bool $quiet = true, array $env = [], ?int $timeout = 3600, mixed $input = null): string + { + $process = $this->setupProcess($args, $dir, $env, $timeout, $input); + $this->runProcess($process, true, $quiet); + return $process->getOutput(); + } + /** * Sets up a Process and reports to the user that the command is being run. */ @@ -169,11 +183,10 @@ private function showEnvMessage(array $env): void * @throws RuntimeException * If the process fails or times out, and $mustRun is true. * - * @return int|bool|string - * The exit code of the process if it fails, true if it succeeds with no - * output, or a string if it succeeds with output. + * @return int + * The exit code of the process. */ - protected function runProcess(Process $process, bool $mustRun = false, bool $quiet = true): int|bool|string + private function runProcess(Process $process, bool $mustRun = false, bool $quiet = true): int { try { $process->mustRun(function ($type, $buffer) use ($quiet): void { @@ -196,9 +209,7 @@ protected function runProcess(Process $process, bool $mustRun = false, bool $qui // will generate a much shorter message. throw new \Platformsh\Cli\Exception\ProcessFailedException($process, $quiet); } - $output = $process->getOutput(); - - return $output ? rtrim($output) : true; + return 0; } /** diff --git a/src/SshCert/Certifier.php b/src/SshCert/Certifier.php index c51d49dc3..96bf80f1c 100644 --- a/src/SshCert/Certifier.php +++ b/src/SshCert/Certifier.php @@ -263,7 +263,7 @@ private function generateSshKey(string $filename): void // encounters existing keys. This seems to be necessary during race // conditions despite deleting keys in advance with $this->fs->remove(). $this->fs->remove([$filename, $filename . '.pub']); - $this->shell->execute($args, null, true, true, [], 60, "y\n"); + $this->shell->mustExecute($args, timeout: 60, input: "y\n"); } /**