diff --git a/integration-tests/autoscaling_enabled_test.go b/integration-tests/autoscaling_enabled_test.go index 9519237c5..644f91d8d 100644 --- a/integration-tests/autoscaling_enabled_test.go +++ b/integration-tests/autoscaling_enabled_test.go @@ -80,6 +80,20 @@ func TestAutoscalingSettingsSetEnabled(t *testing.T) { // setUpAutoscaling configures a project with an "app" and the given services' // autoscaling settings. func setUpAutoscaling(apiHandler *mockapi.Handler, services map[string]any) (projectID string) { + trigger := map[string]any{ + "enabled": false, + "up": map[string]any{"threshold": 80, "duration": 60}, + "down": map[string]any{"threshold": 20, "duration": 600}, + } + return setUpAutoscalingWithDefaults(apiHandler, map[string]any{ + "triggers": map[string]any{"cpu": trigger, "memory": trigger}, + "scale_cooldown": map[string]any{"up": 300, "down": 300}, + "instances": map[string]any{"min": 1, "max": 10}, + }, services) +} + +// setUpAutoscalingWithDefaults is setUpAutoscaling with the given autoscaling defaults. +func setUpAutoscalingWithDefaults(apiHandler *mockapi.Handler, defaults, services map[string]any) (projectID string) { projectID = mockapi.ProjectID() apiHandler.SetProjects([]*mockapi.Project{{ ID: projectID, @@ -117,19 +131,7 @@ func setUpAutoscaling(apiHandler *mockapi.Handler, services map[string]any) (pro }) }) - trigger := map[string]any{ - "enabled": false, - "up": map[string]any{"threshold": 80, "duration": 60}, - "down": map[string]any{"threshold": 20, "duration": 600}, - } - main.SetAutoscalingSettings(map[string]any{ - "defaults": map[string]any{ - "triggers": map[string]any{"cpu": trigger, "memory": trigger}, - "scale_cooldown": map[string]any{"up": 300, "down": 300}, - "instances": map[string]any{"min": 1, "max": 10}, - }, - "services": services, - }) + main.SetAutoscalingSettings(map[string]any{"defaults": defaults, "services": services}) return projectID } diff --git a/integration-tests/autoscaling_get_test.go b/integration-tests/autoscaling_get_test.go new file mode 100644 index 000000000..89bf5d9d2 --- /dev/null +++ b/integration-tests/autoscaling_get_test.go @@ -0,0 +1,107 @@ +package tests + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/upsun/cli/pkg/mockapi" +) + +// TestAutoscalingSettingsGetDirections checks that each direction of a trigger shows whether the trigger is enabled. +func TestAutoscalingSettingsGetDirections(t *testing.T) { + t.Parallel() + apiHandler := mockapi.NewHandler(t) + projectID := setUpAutoscaling(apiHandler, map[string]any{ + "app": map[string]any{ + "triggers": map[string]any{ + "cpu": map[string]any{ + "enabled": true, + "up": map[string]any{"threshold": 80, "duration": 60}, + "down": map[string]any{"threshold": 20, "duration": 300}, + }, + // The memory trigger doesn't say whether it is enabled. + "memory": map[string]any{"up": map[string]any{"threshold": 70, "duration": 60}}, + }, + "instances": map[string]any{"min": 1, "max": 3}, + "scale_cooldown": map[string]any{"up": 300, "down": 600}, + }, + }) + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + stdout, stderr, err := f.RunCombinedOutput("autoscaling:get", "-p", projectID, "-e", "main", "--format", "csv", + "--columns", "service,metric,direction,threshold,duration,enabled,cooldown,min_instances,max_instances") + require.NoError(t, err, stderr) + assertTrimmed(t, ` +App or service,Metric,Direction,Threshold (%),Duration (s),Enabled,Cooldown (s),Minimum instances,Maximum instances +app,cpu,up,80.0%,60,true,300,1,3 +app,cpu,down,20.0%,300,true,600,1,3 +app,memory,up,70.0%,60,,300,1,3 +`, stdout) +} + +// TestAutoscalingSettingsSetPartial checks changes to settings that omit values, e.g. after a partial update. +func TestAutoscalingSettingsSetPartial(t *testing.T) { + t.Parallel() + apiHandler := mockapi.NewHandler(t) + projectID := setUpAutoscaling(apiHandler, map[string]any{ + "app": map[string]any{ + "triggers": map[string]any{ + "cpu": map[string]any{"enabled": true, "up": map[string]any{"threshold": 80}}, + }, + "instances": map[string]any{"max": 3}, + "scale_cooldown": map[string]any{"down": 600}, + }, + }) + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + // Verbose output shows PHP warnings. + _, stderr, err := f.RunCombinedOutput("autoscaling:set", "-v", "-p", projectID, "-e", "main", + "--service", "app", "--metric", "cpu", "--duration-up", "2m", "--cooldown-up", "5m", "--instances-min", "2", + "--dry-run") + require.NoError(t, err, stderr) + assert.NotContains(t, stderr, "Warning") + assert.Contains(t, stderr, "Duration: 2m") + assert.Contains(t, stderr, "Cooldown: 5m") + assert.Contains(t, stderr, "Min: 2") +} + +// TestAutoscalingSettingsSetInteractivePartialDefaults checks the interactive form when the defaults omit a value. +func TestAutoscalingSettingsSetInteractivePartialDefaults(t *testing.T) { + t.Parallel() + apiHandler := mockapi.NewHandler(t) + projectID := setUpAutoscalingWithDefaults(apiHandler, map[string]any{ + "triggers": map[string]any{"cpu": map[string]any{ + "up": map[string]any{"threshold": 80, "duration": 60}, + "down": map[string]any{"threshold": 20, "duration": 600}, + }}, + "instances": map[string]any{"min": 1}, + }, map[string]any{}) + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + // Accept the defaults for the metric, and for each scaling direction's threshold and duration + // (the only app is chosen without a question). The cooldowns and the maximum instance count + // have no default: an empty answer is rejected and the question is asked again. + _, stderr, err := f.RunInteractive("\n\n\n\n5m\n\n\n\n5m\n\n\n3\n", + "autoscaling:set", "-p", projectID, "-e", "main", "--dry-run") + require.NoError(t, err, stderr) + assert.NotContains(t, stderr, "TypeError") + assert.Equal(t, 3, strings.Count(stderr, "A value is required.")) + assert.Contains(t, stderr, "Cooldown: 5m") + assert.Contains(t, stderr, "Max: 3") +} diff --git a/integration-tests/resources_build_test.go b/integration-tests/resources_build_test.go index 1295df536..1790e6b8a 100644 --- a/integration-tests/resources_build_test.go +++ b/integration-tests/resources_build_test.go @@ -13,6 +13,13 @@ import ( // setUpBuildResources serves a project with build resources of 1 CPU and 2048 MB, and maximums of 4 CPU and 8192 MB. func setUpBuildResources(t *testing.T) (apiHandler *mockapi.Handler, f *cmdFactory, projectID string) { + return setUpBuildResourcesWith(t, map[string]any{"cpu": 1, "memory": 2048}) +} + +// setUpBuildResourcesWith is setUpBuildResources with the given build resources settings. +func setUpBuildResourcesWith(t *testing.T, buildResources map[string]any) ( + apiHandler *mockapi.Handler, f *cmdFactory, projectID string, +) { apiHandler = mockapi.NewHandler(t) projectID = mockapi.ProjectID() apiHandler.SetProjects([]*mockapi.Project{{ @@ -24,7 +31,7 @@ func setUpBuildResources(t *testing.T) (apiHandler *mockapi.Handler, f *cmdFacto DefaultBranch: "main", Settings: map[string]any{ "sizing_api_enabled": true, - "build_resources": map[string]any{"cpu": 1, "memory": 2048}, + "build_resources": buildResources, }, }}) apiHandler.SetEnvironments([]*mockapi.Environment{makeEnv(projectID, "main", "production", "active", nil)}) @@ -108,10 +115,28 @@ func TestBuildResourcesSet(t *testing.T) { assert.Contains(t, stderr, c.wantErr) } - _, stderr, err := f.RunCombinedOutput("resources:build:set", "-p", projectID, "--memory", "2048") - require.NoError(t, err, stderr) - assert.Contains(t, stderr, "No changes were provided: nothing to update.") + // The current values are the API's integers. + for _, args := range [][]string{{"--memory", "2048"}, {"--cpu", "1"}} { + _, stderr, err := f.RunCombinedOutput(append([]string{"resources:build:set", "-p", projectID}, args...)...) + require.NoError(t, err, stderr) + assert.Contains(t, stderr, "No changes were provided: nothing to update.") + } assert.Empty(t, apiHandler.ProjectSettingsPatches(projectID)) }) } + +// TestBuildResourcesPartial checks the build resources commands when the settings omit a value. +func TestBuildResourcesPartial(t *testing.T) { + t.Parallel() + apiHandler, f, projectID := setUpBuildResourcesWith(t, map[string]any{"cpu": 1}) + + assertTrimmed(t, "CPU\tMemory (MB)\n1\t", f.Run("resources:build:get", "-p", projectID, "--format", "tsv")) + + _, stderr, err := f.RunCombinedOutput("resources:build:set", "-p", projectID, "--memory", "1024") + require.NoError(t, err, stderr) + assert.Contains(t, stderr, "Memory: 1024 MB") + patches := apiHandler.ProjectSettingsPatches(projectID) + require.Len(t, patches, 1) + assert.Equal(t, map[string]any{"build_resources": map[string]any{"memory": 1024.0}}, patches[0]) +} diff --git a/legacy/phpstan-baseline.neon b/legacy/phpstan-baseline.neon index 11d409fed..b2d773029 100644 --- a/legacy/phpstan-baseline.neon +++ b/legacy/phpstan-baseline.neon @@ -270,12 +270,6 @@ parameters: count: 1 path: src/Application.php - - - message: '#^Parameter \#1 \$timezoneId of function date_default_timezone_set expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Application.php - - message: '#^Parameter \#1 \$values of method Symfony\\Component\\Console\\Completion\\CompletionSuggestions\:\:suggestValues\(\) expects list\, array\ given\.$#' identifier: argument.type @@ -414,174 +408,30 @@ parameters: count: 1 path: src/Command/Auth/BrowserLoginCommand.php - - - message: '#^Binary operation "\." between ''A verification code…'' and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 2 - path: src/Command/Auth/VerifyPhoneNumberCommand.php - - - - message: '#^Binary operation "\." between ''Calling the number …'' and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Command/Auth/VerifyPhoneNumberCommand.php - - - - message: '#^Binary operation "\." between ''E164\-formatted…'' and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Command/Auth/VerifyPhoneNumberCommand.php - - message: '#^Cannot cast mixed to string\.$#' identifier: cast.string count: 1 path: src/Command/Auth/VerifyPhoneNumberCommand.php - - - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Cannot access offset ''duration'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Cannot access offset ''max'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Cannot access offset ''min'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - message: '#^Cannot access offset ''supports_horizontal_scaling_services'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - message: '#^Cannot access offset ''threshold'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Cannot access offset mixed on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Parameter \#1 \$autoscalingSettings of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsGetCommand\:\:filterAutoscalingSettings\(\) expects array\\>, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Parameter \#1 \$rows of method Platformsh\\Cli\\Service\\Table\:\:render\(\) expects array\\|Symfony\\Component\\Console\\Helper\\TableSeparator\>, list\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsGetCommand.php - - - - message: '#^Cannot access offset ''down'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset ''duration'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 4 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset ''enabled'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset ''max'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset ''min'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Cannot access offset ''supports_horizontal_scaling_services'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 2 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - message: '#^Cannot access offset ''threshold'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 4 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Cannot access offset ''threshold\-up'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - message: '#^Cannot access offset ''triggers'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset ''up'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset mixed on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 5 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset string on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 13 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Cannot access offset string\|null on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:askForDurationSetting\(\) should return int but returns mixed\.$#' - identifier: return.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:handleScalingSettings\(\) never assigns null to &\$cooldown so it can be removed from the by\-ref type\.$#' identifier: parameterByRef.unusedType @@ -594,40 +444,10 @@ parameters: count: 1 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - message: '#^Parameter \#1 \$defaults of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:getSupportedMetrics\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#1 \$previousValue of method Platformsh\\Cli\\Service\\ResourcesUtil\:\:formatChange\(\) expects float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 4 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:formatDuration\(\) expects int, mixed given\.$#' identifier: argument.type - count: 8 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:validateDuration\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:validateInstanceCount\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:validateThreshold\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 + count: 4 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - @@ -636,72 +456,12 @@ parameters: count: 1 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - message: '#^Parameter \#2 \$currentServiceSettings of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:handleInstanceSettings\(\) expects array\\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#2 \$limit of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:validateInstanceCount\(\) expects int\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Parameter \#2 \$newValue of method Platformsh\\Cli\\Service\\ResourcesUtil\:\:formatChange\(\) expects float\|int\|string\|null, mixed given\.$#' identifier: argument.type count: 4 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - message: '#^Parameter \#2 \$settings of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:summarizeChanges\(\) expects array\\>, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#3 \$existingValue of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:askForDurationSetting\(\) expects int\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#3 \$instanceLimit of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:handleInstanceSettings\(\) expects int, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#4 \$currentServiceSettings of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:handleScalingSettings\(\) expects array\\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#4 \$defaultValue of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:askForDurationSetting\(\) expects int, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - - message: '#^Parameter \#5 \$defaults of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:handleScalingSettings\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Parameter &\$instancesMax by\-ref type of method Platformsh\\Cli\\Command\\Autoscaling\\AutoscalingSettingsSetCommand\:\:handleInstanceSettings\(\) expects int\|null, mixed given\.$#' identifier: parameterByRef.type @@ -726,12 +486,6 @@ parameters: count: 1 path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 4 - path: src/Command/Autoscaling/AutoscalingSettingsSetCommand.php - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type @@ -1278,24 +1032,12 @@ parameters: count: 1 path: src/Command/Environment/EnvironmentPushCommand.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Command/Environment/EnvironmentPushCommand.php - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Api\:\:getEnvironment\(\) expects string, mixed given\.$#' identifier: argument.type count: 2 path: src/Command/Environment/EnvironmentPushCommand.php - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Environment/EnvironmentPushCommand.php - - message: '#^Parameter \#3 \$target of method Platformsh\\Cli\\Command\\Environment\\EnvironmentPushCommand\:\:determineShouldActivate\(\) expects string, mixed given\.$#' identifier: argument.type @@ -2088,12 +1830,6 @@ parameters: count: 1 path: src/Command/Organization/Billing/OrganizationProfileCommand.php - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 1 - path: src/Command/Organization/OrganizationCommandBase.php - - message: '#^Cannot access offset ''label'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -2118,12 +1854,6 @@ parameters: count: 1 path: src/Command/Organization/OrganizationCreateCommand.php - - - message: '#^Parameter \#1 \$string of function ucfirst expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Organization/OrganizationCreateCommand.php - - message: '#^Parameter \#1 \$string of method Cocur\\Slugify\\Slugify\:\:slugify\(\) expects string, mixed given\.$#' identifier: argument.type @@ -2163,7 +1893,7 @@ parameters: - message: '#^Possibly invalid array key type mixed\.$#' identifier: offsetAccess.invalidOffset - count: 2 + count: 1 path: src/Command/Organization/OrganizationCreateCommand.php - @@ -2239,64 +1969,28 @@ parameters: path: src/Command/Organization/OrganizationSubscriptionListCommand.php - - message: '#^Parameter \#1 \$args of static method Platformsh\\Cli\\Console\\ArrayArgument\:\:split\(\) expects array\, array\ given\.$#' + message: '#^Parameter \#1 \$permissions of method Platformsh\\Cli\\Command\\Organization\\User\\OrganizationUserCommandBase\:\:listPermissions\(\) expects array\\|null, mixed given\.$#' identifier: argument.type count: 1 path: src/Command/Organization/User/OrganizationUserAddCommand.php - - message: '#^Parameter \#1 \$argument of static method Platformsh\\Cli\\Util\\OsUtil\:\:escapeShellArg\(\) expects string, mixed given\.$#' - identifier: argument.type + message: '#^Cannot access offset string on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 1 - path: src/Command/Organization/User/OrganizationUserAddCommand.php + path: src/Command/Organization/User/OrganizationUserGetCommand.php - - message: '#^Parameter \#1 \$email of method Platformsh\\Client\\Model\\Organization\\Organization\:\:inviteMemberByEmail\(\) expects string, mixed given\.$#' + message: '#^Parameter \#2 \$data of method Platformsh\\Cli\\Service\\PropertyFormatter\:\:displayData\(\) expects array\, array given\.$#' identifier: argument.type count: 1 - path: src/Command/Organization/User/OrganizationUserAddCommand.php + path: src/Command/Organization/User/OrganizationUserGetCommand.php - - message: '#^Parameter \#1 \$permissions of method Platformsh\\Cli\\Command\\Organization\\User\\OrganizationUserCommandBase\:\:listPermissions\(\) expects array\\|null, mixed given\.$#' - identifier: argument.type + message: '#^Cannot access offset ''sort'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible count: 1 - path: src/Command/Organization/User/OrganizationUserAddCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Organization\\User\\OrganizationUserAddCommand\:\:validateEmail\(\) expects string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Organization/User/OrganizationUserAddCommand.php - - - - message: '#^Parameter \#2 \$email of method Platformsh\\Cli\\Service\\Api\:\:loadMemberByEmail\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Organization/User/OrganizationUserAddCommand.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Organization/User/OrganizationUserAddCommand.php - - - - message: '#^Cannot access offset string on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Organization/User/OrganizationUserGetCommand.php - - - - message: '#^Parameter \#2 \$data of method Platformsh\\Cli\\Service\\PropertyFormatter\:\:displayData\(\) expects array\, array given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Organization/User/OrganizationUserGetCommand.php - - - - message: '#^Cannot access offset ''sort'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Organization/User/OrganizationUserListCommand.php + path: src/Command/Organization/User/OrganizationUserListCommand.php - message: '#^Cannot access offset ''filter\[organization_id\]'' on mixed\.$#' @@ -2512,173 +2206,11 @@ parameters: message: '#^Cannot access offset ''cpu'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 - path: src/Command/Resources/Build/BuildResourcesGetCommand.php - - - - message: '#^Cannot access offset ''memory'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/Build/BuildResourcesGetCommand.php - - - - message: '#^Parameter \#1 \$rows of method Platformsh\\Cli\\Service\\Table\:\:render\(\) expects array\\|Symfony\\Component\\Console\\Helper\\TableSeparator\>, array\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/Build/BuildResourcesGetCommand.php - - - - message: '#^Cannot access offset ''cpu'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - message: '#^Cannot access offset ''max_cpu'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Cannot access offset ''max_memory'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Cannot access offset ''memory'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Cannot cast mixed to float\.$#' - identifier: cast.double - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Cannot cast mixed to int\.$#' - identifier: cast.int - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$unformatted of method Platformsh\\Cli\\Service\\ResourcesUtil\:\:formatCPU\(\) expects float\|int\|string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$updates of method Platformsh\\Cli\\Command\\Resources\\Build\\BuildResourcesSetCommand\:\:summarizeChanges\(\) expects array\{cpu\?\: int, memory\?\: int\}, non\-empty\-array\{cpu\?\: mixed, memory\?\: mixed\} given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$current of method Platformsh\\Cli\\Command\\Resources\\Build\\BuildResourcesSetCommand\:\:summarizeChanges\(\) expects array\{cpu\: int, memory\: int\}, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Resources/Build/BuildResourcesSetCommand.php - - - - message: '#^Cannot access offset ''disk'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesCommandBase.php - - - - message: '#^Cannot access offset ''minimum'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesCommandBase.php - - - - message: '#^Cannot access offset ''profile_size'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesCommandBase.php - - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 4 - path: src/Command/Resources/ResourcesCommandBase.php - - - - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable - count: 2 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Cannot access offset ''disk'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Cannot access offset ''enabled'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Cannot access offset ''object'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Cannot access offset ''profile_size'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Parameter \#1 \$profiles of method Platformsh\\Cli\\Command\\Resources\\ResourcesCommandBase\:\:sortContainerProfiles\(\) expects array\\>\>, array given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Parameter \#1 \$rows of method Platformsh\\Cli\\Service\\Table\:\:render\(\) expects array\\|Symfony\\Component\\Console\\Helper\\TableSeparator\>, list\\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesGetCommand.php - - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 6 - path: src/Command/Resources/ResourcesGetCommand.php - - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable - count: 4 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Binary operation "\*" between mixed and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 4 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Binary operation "\-" between mixed and mixed results in an error\.$#' - identifier: binaryOp.invalid count: 1 path: src/Command/Resources/ResourcesSetCommand.php @@ -2688,70 +2220,40 @@ parameters: count: 1 path: src/Command/Resources/ResourcesSetCommand.php - - - message: '#^Cannot access offset ''capabilities'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - message: '#^Cannot access offset ''cpu'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 7 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Cannot access offset ''cpu_type'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 + count: 2 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Cannot access offset ''default'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 4 + count: 2 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Cannot access offset ''disk'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 8 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Cannot access offset ''enabled'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Cannot access offset ''instance_limit'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 + count: 3 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Cannot access offset ''memory'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 7 + count: 2 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Cannot access offset ''minimum'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 8 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Cannot access offset ''object'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 + count: 5 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Cannot access offset ''profile_size'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible - count: 15 + count: 2 path: src/Command/Resources/ResourcesSetCommand.php - @@ -2766,12 +2268,6 @@ parameters: count: 1 path: src/Command/Resources/ResourcesSetCommand.php - - - message: '#^Cannot access offset mixed on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 4 - path: src/Command/Resources/ResourcesSetCommand.php - - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int @@ -2781,7 +2277,7 @@ parameters: - message: '#^Cannot cast mixed to string\.$#' identifier: cast.string - count: 4 + count: 1 path: src/Command/Resources/ResourcesSetCommand.php - @@ -2808,166 +2304,22 @@ parameters: count: 1 path: src/Command/Resources/ResourcesSetCommand.php - - - message: '#^Parameter \#1 \$previousValue of method Platformsh\\Cli\\Service\\ResourcesUtil\:\:formatChange\(\) expects float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 3 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$properties of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:diskIsRequiredButUnset\(\) expects array\, array given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$properties of method Platformsh\\Cli\\Service\\ResourcesUtil\:\:sizeInfo\(\) expects array\, array given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$updates of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:computeMemoryCPUStorageDiff\(\) expects array\\>\>, array\\>\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$updates of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:summarizeChanges\(\) expects array\\>\>, array\\>\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateDiskSize\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateInstanceCount\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateObjectStorage\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateProfileSize\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$current of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:computeMemoryCPUStorageDiff\(\) expects array\\>\>, array\\>\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$newValue of method Platformsh\\Cli\\Service\\ResourcesUtil\:\:formatChange\(\) expects float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 3 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$serviceName of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateDiskSize\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$serviceName of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateInstanceCount\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$serviceName of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateObjectStorage\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$serviceName of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateProfileSize\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#2 \$services of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:summarizeChanges\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type - count: 7 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$containerProfiles of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:summarizeChanges\(\) expects array\, array given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$default of method Platformsh\\Cli\\Service\\QuestionHelper\:\:chooseAssoc\(\) expects string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$service of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateDiskSize\(\) expects Platformsh\\Client\\Model\\Deployment\\Service\|Platformsh\\Client\\Model\\Deployment\\Task\|Platformsh\\Client\\Model\\Deployment\\WebApp\|Platformsh\\Client\\Model\\Deployment\\Worker, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$service of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateInstanceCount\(\) expects Platformsh\\Client\\Model\\Deployment\\Service\|Platformsh\\Client\\Model\\Deployment\\Task\|Platformsh\\Client\\Model\\Deployment\\WebApp\|Platformsh\\Client\\Model\\Deployment\\Worker, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$service of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateObjectStorage\(\) expects Platformsh\\Client\\Model\\Deployment\\Service\|Platformsh\\Client\\Model\\Deployment\\Task\|Platformsh\\Client\\Model\\Deployment\\WebApp\|Platformsh\\Client\\Model\\Deployment\\Worker, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$service of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateProfileSize\(\) expects Platformsh\\Client\\Model\\Deployment\\Service\|Platformsh\\Client\\Model\\Deployment\\Task\|Platformsh\\Client\\Model\\Deployment\\WebApp\|Platformsh\\Client\\Model\\Deployment\\Worker, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#3 \$services of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:parseSetting\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 4 + count: 3 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type - count: 5 - path: src/Command/Resources/ResourcesSetCommand.php - - - - message: '#^Parameter \#4 \$limit of method Platformsh\\Cli\\Command\\Resources\\ResourcesSetCommand\:\:validateInstanceCount\(\) expects int\|null, mixed given\.$#' - identifier: argument.type - count: 2 + count: 3 path: src/Command/Resources/ResourcesSetCommand.php - message: '#^Parameter \#4 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type - count: 2 + count: 1 path: src/Command/Resources/ResourcesSetCommand.php - @@ -2979,15 +2331,9 @@ parameters: - message: '#^Possibly invalid array key type mixed\.$#' identifier: offsetAccess.invalidOffset - count: 9 + count: 2 path: src/Command/Resources/ResourcesSetCommand.php - - - message: '#^Parameter \#1 \$profiles of method Platformsh\\Cli\\Command\\Resources\\ResourcesCommandBase\:\:sortContainerProfiles\(\) expects array\\>\>, array given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Resources/ResourcesSizeListCommand.php - - message: '#^Parameter \#1 \$routes of static method Platformsh\\Cli\\Model\\Route\:\:fromVariables\(\) expects array\, mixed given\.$#' identifier: argument.type @@ -3282,54 +2628,6 @@ parameters: count: 1 path: src/Command/Service/ServiceListCommand.php - - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Config\:\:setSessionId\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Session/SessionSwitchCommand.php - - - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Config\:\:validateSessionId\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Session/SessionSwitchCommand.php - - - - message: '#^Binary operation "\." between ''The file already…'' and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Command/SshKey/SshKeyAddCommand.php - - - - message: '#^Method Platformsh\\Cli\\Command\\SshKey\\SshKeyAddCommand\:\:askNewKeyPath\(\) should return string but returns mixed\.$#' - identifier: return.type - count: 1 - path: src/Command/SshKey/SshKeyAddCommand.php - - - - message: '#^Parameter \#1 \$filename of function file_exists expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/SshKey/SshKeyAddCommand.php - - - - message: '#^Parameter \#1 \$haystack of function str_starts_with expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/SshKey/SshKeyAddCommand.php - - - - message: '#^Parameter \#1 \$string of function ltrim expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/SshKey/SshKeyAddCommand.php - - - - message: '#^Parameter \#1 \$string of function substr expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/SshKey/SshKeyAddCommand.php - - message: '#^Cannot access offset ''command'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -3372,36 +2670,6 @@ parameters: count: 1 path: src/Command/Team/Project/TeamProjectAddCommand.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Command/Team/Project/TeamProjectAddCommand.php - - - - message: '#^Parameter \#1 \$project of method Platformsh\\Cli\\Service\\Api\:\:getProjectLabel\(\) expects Platformsh\\Client\\Model\\BasicProjectInfo\|Platformsh\\Client\\Model\\Organization\\Project\|Platformsh\\Client\\Model\\Project\|Platformsh\\Client\\Model\\Team\\TeamProjectAccess\|string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Team/Project/TeamProjectAddCommand.php - - - - message: '#^Parameter \#1 \$projectIds of method Platformsh\\Cli\\Command\\Team\\Project\\TeamProjectAddCommand\:\:displayProjectsAsList\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Team/Project/TeamProjectAddCommand.php - - - - message: '#^Parameter \#1 \$projectIds of method Platformsh\\Cli\\Command\\Team\\Project\\TeamProjectAddCommand\:\:displayProjectsAsList\(\) expects array\, list\ given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Team/Project/TeamProjectAddCommand.php - - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 1 - path: src/Command/Team/Project/TeamProjectAddCommand.php - - message: '#^Cannot access offset ''sort'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -3414,12 +2682,6 @@ parameters: count: 1 path: src/Command/Team/TeamCreateCommand.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Command/Team/TeamCreateCommand.php - - message: '#^Method Platformsh\\Cli\\Command\\Team\\TeamCreateCommand\:\:showProjectRoleForm\(\) should return string but returns mixed\.$#' identifier: return.type @@ -3438,12 +2700,6 @@ parameters: count: 1 path: src/Command/Team/TeamCreateCommand.php - - - message: '#^Parameter \#1 \$label of method Platformsh\\Client\\Model\\Organization\\Organization\:\:createTeam\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Team/TeamCreateCommand.php - - message: '#^Parameter \#1 \$roles of method Platformsh\\Cli\\Command\\Team\\TeamCreateCommand\:\:describeRoleInput\(\) expects array\, array given\.$#' identifier: argument.type @@ -3475,21 +2731,9 @@ parameters: path: src/Command/Team/TeamCreateCommand.php - - message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Team/TeamCreateCommand.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Team/TeamCreateCommand.php - - - - message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' + message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' identifier: argument.type - count: 2 + count: 1 path: src/Command/Team/TeamCreateCommand.php - @@ -3546,30 +2790,6 @@ parameters: count: 1 path: src/Command/Team/TeamListCommand.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Command/Team/User/TeamUserAddCommand.php - - - - message: '#^Parameter \#1 \$argument of static method Platformsh\\Cli\\Util\\OsUtil\:\:escapeShellArg\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Team/User/TeamUserAddCommand.php - - - - message: '#^Parameter \#2 \$email of method Platformsh\\Cli\\Service\\Api\:\:loadMemberByEmail\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Command/Team/User/TeamUserAddCommand.php - - - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Command/Team/User/TeamUserAddCommand.php - - message: '#^Cannot access offset ''page\[size\]'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -4716,12 +3936,6 @@ parameters: count: 1 path: src/Selector/Selector.php - - - message: '#^Method Platformsh\\Cli\\Selector\\Selector\:\:offerProjectChoice\(\) should return string but returns mixed\.$#' - identifier: return.type - count: 1 - path: src/Selector/Selector.php - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Api\:\:getEnvironment\(\) expects string, mixed given\.$#' identifier: argument.type @@ -4764,12 +3978,6 @@ parameters: count: 1 path: src/Selector/Selector.php - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 1 - path: src/Selector/Selector.php - - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable @@ -5018,409 +4226,163 @@ parameters: - message: '#^Method Platformsh\\Cli\\Service\\Api\:\:listMembers\(\) should return array\ but returns mixed\.$#' - identifier: return.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$array of static method Platformsh\\Cli\\Util\\NestedArrayUtil\:\:getNestedArrayValue\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$authMethods of class Platformsh\\Cli\\Event\\LoginRequiredEvent constructor expects array\, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Deployment\\EnvironmentDeployment constructor expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Environment constructor expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\EnvironmentType constructor expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Organization\\Organization constructor expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Project constructor expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\User constructor expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$data of method Platformsh\\Cli\\Service\\Api\:\:isSsoSessionExpired\(\) expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Api\:\:getOrganizationById\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$id of method Platformsh\\Client\\PlatformClient\:\:getOrganizationById\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$storage of method Platformsh\\Client\\Session\\Session\:\:setStorage\(\) expects Platformsh\\Client\\Session\\Storage\\SessionStorageInterface, Platformsh\\Client\\Session\\Storage\\SessionStorageInterface\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$string of function strlen expects string, mixed given\.$#' - identifier: argument.type - count: 2 - path: src/Service/Api.php - - - - message: '#^Parameter \#1 \$url of method Platformsh\\Client\\Model\\Project\:\:setApiUrl\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\Deployment\\EnvironmentDeployment constructor expects string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\EnvironmentType constructor expects string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\Organization\\Organization constructor expects string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\Project constructor expects string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \$maxAge of class Platformsh\\Cli\\Event\\LoginRequiredEvent constructor expects int\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#2 \.\.\.\$arrays of function array_diff_key expects array, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Api.php - - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 2 - path: src/Service/Api.php - - - - message: '#^Strict comparison using \!\=\= between string and null will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: src/Service/Api.php - - - - message: '#^Binary operation "\." between mixed and ''\-Cluster'' results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Service/Config.php - - - - message: '#^Binary operation "\." between mixed and ''/''\|''\\\\'' results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Service/Config.php - - - - message: '#^Binary operation "\." between mixed and ''HOME'' results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Service/Config.php - - - - message: '#^Binary operation "\." between mixed and string results in an error\.$#' - identifier: binaryOp.invalid - count: 3 - path: src/Service/Config.php - - - - message: '#^Binary operation "\." between non\-falsy\-string and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''add_to_agent'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''add_to_ssh_agent'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''all_experiments'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''applications_config_file'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''archive_dir''\|''build_dir''\|''dependencies_dir''\|''project_config''\|''shared_dir'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''auto_load_cert'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''auto_load_ssh_cert'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''certifier_url''\|''oauth2_auth_url''\|''oauth2_revoke_url''\|''oauth2_token_url'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''cluster_header'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''console_domain'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php + identifier: return.type + count: 1 + path: src/Service/Api.php - - message: '#^Cannot access offset ''console_url'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$array of static method Platformsh\\Cli\\Util\\NestedArrayUtil\:\:getNestedArrayValue\(\) expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''disabled_commands'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$authMethods of class Platformsh\\Cli\\Event\\LoginRequiredEvent constructor expects array\, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''domain_wildcards'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 - path: src/Service/Config.php + message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Deployment\\EnvironmentDeployment constructor expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Service/Api.php - - message: '#^Cannot access offset ''enable_commands'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Environment constructor expects array, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''env_prefix'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php + message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\EnvironmentType constructor expects array, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Service/Api.php - - message: '#^Cannot access offset ''experimental_commands'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Organization\\Organization constructor expects array, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''header_prefix'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\Project constructor expects array, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''hidden_commands'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$data of class Platformsh\\Client\\Model\\User constructor expects array, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''local_dir'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php + message: '#^Parameter \#1 \$data of method Platformsh\\Cli\\Service\\Api\:\:isSsoSessionExpired\(\) expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Service/Api.php - - message: '#^Cannot access offset ''oauth2_client_id'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Api\:\:getOrganizationById\(\) expects string, mixed given\.$#' + identifier: argument.type count: 2 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''organization_types'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$id of method Platformsh\\Client\\PlatformClient\:\:getOrganizationById\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''prompt'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$storage of method Platformsh\\Client\\Session\\Session\:\:setStorage\(\) expects Platformsh\\Client\\Session\\Storage\\SessionStorageInterface, Platformsh\\Client\\Session\\Storage\\SessionStorageInterface\|null given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''session_id'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 3 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''slug'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$string of function strlen expects string, mixed given\.$#' + identifier: argument.type count: 2 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''ssh_domain_wildcards'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#1 \$url of method Platformsh\\Client\\Model\\Project\:\:setApiUrl\(\) expects string, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php - - - - message: '#^Cannot access offset ''tmp_sub_dir'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''wrapped_disabled_commands'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset ''writable_user_dir'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\Deployment\\EnvironmentDeployment constructor expects string\|null, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot access offset string on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible + message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\EnvironmentType constructor expects string\|null, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 4 - path: src/Service/Config.php + message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\Organization\\Organization constructor expects string\|null, mixed given\.$#' + identifier: argument.type + count: 1 + path: src/Service/Api.php - - message: '#^Method Platformsh\\Cli\\Service\\Config\:\:get\(\) should return array\\|bool\|float\|int\|string\|null but returns mixed\.$#' - identifier: return.type + message: '#^Parameter \#2 \$baseUrl of class Platformsh\\Client\\Model\\Project constructor expects string\|null, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Method Platformsh\\Cli\\Service\\Config\:\:getProxies\(\) should return array\{https\?\: string, http\?\: string\} but returns array\{\}\|array\{https\?\: string\|false, http\?\: string\|false\}\.$#' - identifier: return.type + message: '#^Parameter \#2 \$maxAge of class Platformsh\\Cli\\Event\\LoginRequiredEvent constructor expects int\|null, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Method Platformsh\\Cli\\Service\\Config\:\:getSessionId\(\) should return string but returns mixed\.$#' - identifier: return.type + message: '#^Parameter \#2 \.\.\.\$arrays of function array_diff_key expects array, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Method Platformsh\\Cli\\Service\\Config\:\:getVersion\(\) should return string but returns mixed\.$#' - identifier: return.type + message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' + identifier: argument.type count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Method Platformsh\\Cli\\Service\\Config\:\:loadConfigFromFile\(\) should return array\ but returns array\\.$#' - identifier: return.type - count: 1 - path: src/Service/Config.php + message: '#^Possibly invalid array key type mixed\.$#' + identifier: offsetAccess.invalidOffset + count: 2 + path: src/Service/Api.php - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Config\:\:validateSessionId\(\) expects string, mixed given\.$#' - identifier: argument.type + message: '#^Strict comparison using \!\=\= between string and null will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue count: 1 - path: src/Service/Config.php + path: src/Service/Api.php - - message: '#^Parameter \#2 \$haystack of function in_array expects array, mixed given\.$#' - identifier: argument.type - count: 5 + message: '#^Method Platformsh\\Cli\\Service\\Config\:\:get\(\) should return array\\|bool\|float\|int\|string\|null but returns mixed\.$#' + identifier: return.type + count: 1 path: src/Service/Config.php - - message: '#^Parameter \#3 \$subject of function str_replace expects array\\|string, mixed given\.$#' - identifier: argument.type + message: '#^Method Platformsh\\Cli\\Service\\Config\:\:loadConfigFromFile\(\) should return array\ but returns array\\.$#' + identifier: return.type count: 1 path: src/Service/Config.php @@ -5430,12 +4392,6 @@ parameters: count: 2 path: src/Service/Config.php - - - message: '#^Property Platformsh\\Cli\\Service\\Config\:\:\$version \(string\|null\) does not accept mixed\.$#' - identifier: assign.propertyType - count: 1 - path: src/Service/Config.php - - message: '#^Method Platformsh\\Cli\\Service\\CountryService\:\:listCountries\(\) should return array\ but returns mixed\.$#' identifier: return.type @@ -5616,24 +4572,12 @@ parameters: count: 1 path: src/Service/Identifier.php - - - message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, ''preg_quote'' given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Identifier.php - - message: '#^Method Platformsh\\Cli\\Service\\InstallationInfo\:\:cliPath\(\) should return string but returns mixed\.$#' identifier: return.type count: 1 path: src/Service/InstallationInfo.php - - - message: '#^Binary operation "\-" between int\<1, max\> and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Service/LegacyMigration.php - - message: '#^Cannot access offset ''3\.x'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -5670,12 +4614,6 @@ parameters: count: 1 path: src/Service/PropertyFormatter.php - - - message: '#^Parameter \#1 \$timezone of class DateTimeZone constructor expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/PropertyFormatter.php - - message: '#^Parameter \#1 \$value of method Platformsh\\Cli\\Service\\PropertyFormatter\:\:formatDate\(\) expects int\|string, mixed given\.$#' identifier: argument.type @@ -5880,54 +4818,6 @@ parameters: count: 1 path: src/Service/RemoteEnvVars.php - - - message: '#^Cannot access offset ''disk'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Cannot access offset ''minimum'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Cannot access offset ''profile_size'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Cannot access offset mixed on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Method Platformsh\\Cli\\Service\\ResourcesUtil\:\:allServices\(\) should return array\ but returns array\\.$#' - identifier: return.type - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Method Platformsh\\Cli\\Service\\ResourcesUtil\:\:sizeInfo\(\) should return array\{cpu\: string, memory\: string\}\|null but returns mixed\.$#' - identifier: return.type - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Parameter \#1 \$id of method Platformsh\\Cli\\Service\\Api\:\:getOrganizationById\(\) expects string, mixed given\.$#' - identifier: argument.type - count: 1 - path: src/Service/ResourcesUtil.php - - - - message: '#^Possibly invalid array key type mixed\.$#' - identifier: offsetAccess.invalidOffset - count: 2 - path: src/Service/ResourcesUtil.php - - message: '#^Method Platformsh\\Cli\\Service\\Rsync\:\:supportsConvertingFilenames\(\) should return bool\|null but returns mixed\.$#' identifier: return.type @@ -5970,48 +4860,18 @@ parameters: count: 1 path: src/Service/Ssh.php - - - message: '#^Method Platformsh\\Cli\\Service\\Ssh\:\:getSshOptions\(\) should return array\ but returns array\\.$#' - identifier: return.type - count: 1 - path: src/Service/Ssh.php - - - - message: '#^Parameter \#1 \$array of function array_unique expects an array of values castable to string, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Service/Ssh.php - - message: '#^Parameter \#3 \$subject of function str_replace expects array\\|string, mixed given\.$#' identifier: argument.type count: 1 path: src/Service/Ssh.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Service/SshConfig.php - - - - message: '#^PHPDoc tag @var with type array\\|string is not subtype of native type non\-empty\-array\|float\|int\\|int\<1, max\>\|non\-falsy\-string\|true\.$#' - identifier: varTag.nativeType - count: 1 - path: src/Service/SshConfig.php - - message: '#^Parameter \#2 \$array of function implode expects array, mixed given\.$#' identifier: argument.type count: 4 path: src/Service/SshConfig.php - - - message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' - identifier: argument.type - count: 1 - path: src/Service/SshConfig.php - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type @@ -6036,30 +4896,12 @@ parameters: count: 1 path: src/Service/SshKey.php - - - message: '#^Binary operation "\." between non\-falsy\-string and mixed results in an error\.$#' - identifier: binaryOp.invalid - count: 1 - path: src/Service/State.php - - message: '#^Property Platformsh\\Cli\\Service\\State\:\:\$state \(array\\) does not accept mixed\.$#' identifier: assign.propertyType count: 1 path: src/Service/State.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 2 - path: src/Service/TokenConfig.php - - - - message: '#^Method Platformsh\\Cli\\Service\\TokenConfig\:\:getAccessToken\(\) should return string\|null but returns mixed\.$#' - identifier: return.type - count: 1 - path: src/Service/TokenConfig.php - - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int diff --git a/legacy/src/Application.php b/legacy/src/Application.php index 72523e26a..3d7ea4110 100644 --- a/legacy/src/Application.php +++ b/legacy/src/Application.php @@ -70,8 +70,7 @@ public function __construct(?Config $config = null) // Use the configured timezone, or fall back to the system timezone. date_default_timezone_set( - $this->config->getWithDefault('application.timezone', null) - ?: TimezoneUtil::getTimezone(), + $this->config->getStrOrNull('application.timezone') ?: TimezoneUtil::getTimezone(), ); // Set the Config service. diff --git a/legacy/src/Command/Auth/BrowserLoginCommand.php b/legacy/src/Command/Auth/BrowserLoginCommand.php index c7b609607..4219835d2 100644 --- a/legacy/src/Command/Auth/BrowserLoginCommand.php +++ b/legacy/src/Command/Auth/BrowserLoginCommand.php @@ -331,7 +331,7 @@ private function getAccessToken(string $authCode, string $codeVerifier, string $ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', ], - 'auth' => [$this->config->get('api.oauth2_client_id'), ''], + 'auth' => [$this->config->getStr('api.oauth2_client_id'), ''], ]); return (array) Utils::jsonDecode((string) $response->getBody(), true); diff --git a/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php b/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php index 3bde3d6ee..7359c463a 100644 --- a/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php +++ b/legacy/src/Command/Autoscaling/AutoscalingSettingsGetCommand.php @@ -9,6 +9,7 @@ use Platformsh\Cli\Service\ResourcesUtil; use Platformsh\Cli\Selector\Selector; use Platformsh\Cli\Command\CommandBase; +use Platformsh\Cli\Model\AutoscalingSettings; use Platformsh\Cli\Service\Table; use Platformsh\Client\Exception\EnvironmentStateException; use Platformsh\Client\Model\Deployment\Service; @@ -17,6 +18,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * @phpstan-import-type ServiceSettings from AutoscalingSettings + */ #[AsCommand(name: 'autoscaling:get', description: 'View the autoscaling configuration of apps, workers, and services on an environment', aliases: ['autoscaling'])] class AutoscalingSettingsGetCommand extends CommandBase { @@ -74,7 +78,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->stdErr->writeln(\sprintf('Autoscaling support is not currently available on the environment: %s', $this->api->getEnvironmentLabel($environment, 'error'))); return 1; } - $autoscalingSettings = $autoscalingSettings->getData(); $services = $this->resourcesUtil->allServices($deployment); // Autoscaling does not apply to tasks. @@ -84,9 +87,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - if (!empty($autoscalingSettings['services'])) { + if (!empty($autoscalingSettings->services)) { // Filter autoscaling settings to only show services that are allowed to be configured. - $filteredSettings = $this->filterAutoscalingSettings($autoscalingSettings['services'], $services, $selection->getProject()); + $filteredSettings = $this->filterAutoscalingSettings($autoscalingSettings->services, $services, $selection->getProject()); if (empty($filteredSettings)) { $this->stdErr->writeln(sprintf('No autoscaling configuration found for the project %s, environment %s.', $this->api->getProjectLabel($selection->getProject()), $this->api->getEnvironmentLabel($environment))); @@ -119,13 +122,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'instance_count' => $empty, ]; - foreach ($settings['triggers'] as $metric => $conditions) { + foreach ($settings['triggers'] as $metric => $trigger) { $row['metric'] = $metric; - foreach ($conditions as $direction => $condition) { - if ($direction === 'enabled') { - $row['enabled'] = $this->propertyFormatter->format($condition, 'enabled'); + $row['enabled'] = isset($trigger['enabled']) ? $this->propertyFormatter->format($trigger['enabled'], 'enabled') : $empty; + foreach (['up', 'down'] as $direction) { + if (!isset($trigger[$direction])) { continue; } + $condition = $trigger[$direction]; $row['direction'] = $direction; $row['threshold'] = isset($condition['threshold']) ? sprintf('%.1f%%', $condition['threshold']) : $empty; $row['duration'] = $condition['duration'] ?? $empty; @@ -160,11 +164,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Filters autoscaling settings to only include services that are allowed to be configured. * - * @param array> $autoscalingSettings Autoscaling settings from the API + * @param array $autoscalingSettings Autoscaling settings from the API * @param array $services Array of Service|WebApp|Worker objects from deployment * @param \Platformsh\Client\Model\Project $project * - * @return array> Filtered autoscaling settings + * @return array Filtered autoscaling settings */ protected function filterAutoscalingSettings(array $autoscalingSettings, array $services, \Platformsh\Client\Model\Project $project): array { diff --git a/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php b/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php index ad1d3f937..1c1a8d214 100644 --- a/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php +++ b/legacy/src/Command/Autoscaling/AutoscalingSettingsSetCommand.php @@ -12,6 +12,7 @@ use Platformsh\Cli\Selector\Selector; use Platformsh\Cli\Service\SubCommandRunner; use Platformsh\Cli\Command\CommandBase; +use Platformsh\Cli\Model\AutoscalingSettings; use Platformsh\Client\Exception\EnvironmentStateException; use Platformsh\Client\Model\Deployment\Service; use Platformsh\Client\Model\Deployment\Task; @@ -22,6 +23,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @phpstan-import-type ServiceSettings from AutoscalingSettings + */ #[AsCommand(name: 'autoscaling:set', description: 'Set the autoscaling configuration of apps, workers, or services in an environment')] class AutoscalingSettingsSetCommand extends CommandBase { @@ -93,7 +97,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$autoscalingSettings) { throw new EnvironmentStateException('Managing autoscaling settings is not currently available', $environment); } - $autoscalingSettings = $autoscalingSettings->getData(); $services = $this->resourcesUtil->allServices($deployment); // Autoscaling does not apply to tasks. @@ -104,8 +107,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Get autoscaling default values - $defaults = $autoscalingSettings['defaults'] ?? null; - if (!is_array($defaults)) { + $defaults = $autoscalingSettings->defaults; + if ($defaults === null) { $this->stdErr->writeln('The autoscaling API did not return any default settings for this environment.'); return 1; } @@ -160,7 +163,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Validate the --instances-* options. - $instanceLimit = $defaults['instances']['max']; + $instanceLimit = $defaults['instances']['max'] ?? null; $instancesMin = Option::stringOrNull($input, 'instances-min'); if ($instancesMin !== null) { $instancesMin = $this->validateInstanceCount($instancesMin, $instanceLimit, 'instances-min'); @@ -214,7 +217,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $service = $this->questionHelper->choose($serviceNamesIndexed, $text, $default); // Get autoscaling current values for selected service - $currentServiceSettings = $autoscalingSettings['services'][$service] ?? null; + $currentServiceSettings = $autoscalingSettings->services[$service] ?? null; $this->stdErr->writeln('' . ucfirst($this->typeName($services[$service])) . ': ' . $service . ''); $this->stdErr->writeln(''); @@ -286,7 +289,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Only mark 'enabled' as an updated field if it was explicitly set and is changing - $currentServiceSettings = $autoscalingSettings['services'][$service] ?? null; + $currentServiceSettings = $autoscalingSettings->services[$service] ?? null; if ($enabled !== null && ($currentServiceSettings['triggers'][$metric]['enabled'] ?? false) !== $enabled) { $updates[$service]['enabled'] = $enabled; } @@ -308,7 +311,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - $this->summarizeChanges($updates, $autoscalingSettings['services']); + $this->summarizeChanges($updates, $autoscalingSettings->services); $this->io->debug('Raw updates: ' . json_encode($updates, JSON_UNESCAPED_SLASHES)); @@ -335,8 +338,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Handle scaling settings (up/down) for interactive mode. * - * @param array|null $currentServiceSettings - * @param array $defaults + * @param 'up'|'down' $direction + * @param ServiceSettings|null $currentServiceSettings + * @param ServiceSettings $defaults * @param array> $updates */ private function handleScalingSettings( @@ -359,9 +363,9 @@ private function handleScalingSettings( $threshold, 'Enter the threshold (%)', $currentServiceSettings['triggers'][$metric][$direction]['threshold'] ?? null, - $defaults['triggers'][$metric][$direction]['threshold'], - function ($value) { - return $this->validateThreshold($value); + $defaults['triggers'][$metric][$direction]['threshold'] ?? null, + function (?string $value) { + return $this->validateThreshold($this->requireAnswer($value)); }, $service, 'threshold-' . $direction, @@ -372,7 +376,7 @@ function ($value) { $duration, 'Enter the duration of the evaluation period', $currentServiceSettings['triggers'][$metric][$direction]['duration'] ?? null, - $defaults['triggers'][$metric][$direction]['duration'], + $defaults['triggers'][$metric][$direction]['duration'] ?? null, $service, 'duration-' . $direction, $updates @@ -382,7 +386,7 @@ function ($value) { $cooldown, 'Enter the duration of the cool-down period', $currentServiceSettings['scale_cooldown'][$direction] ?? null, - $defaults['scale_cooldown'][$direction], + $defaults['scale_cooldown'][$direction] ?? null, $service, 'cooldown-' . $direction, $updates @@ -393,13 +397,13 @@ function ($value) { /** * Handle instance settings for interactive mode. * - * @param array|null $currentServiceSettings + * @param ServiceSettings|null $currentServiceSettings * @param array> $updates */ private function handleInstanceSettings( string $service, ?array $currentServiceSettings, - int $instanceLimit, + ?int $instanceLimit, ?int &$instancesMin, ?int &$instancesMax, array &$updates @@ -409,8 +413,8 @@ private function handleInstanceSettings( 'Enter the minimum number of instances', $currentServiceSettings['instances']['min'] ?? null, 1, - function ($value) use ($instanceLimit) { - return $this->validateInstanceCount($value, $instanceLimit); + function (?string $value) use ($instanceLimit) { + return $this->validateInstanceCount($this->requireAnswer($value), $instanceLimit); }, $service, 'instances-min', @@ -422,8 +426,8 @@ function ($value) use ($instanceLimit) { 'Enter the maximum number of instances', $currentServiceSettings['instances']['max'] ?? null, $instanceLimit, - function ($value) use ($instanceLimit) { - return $this->validateInstanceCount($value, $instanceLimit); + function (?string $value) use ($instanceLimit) { + return $this->validateInstanceCount($this->requireAnswer($value), $instanceLimit); }, $service, 'instances-max', @@ -479,7 +483,7 @@ private function askForSetting( * @param int|null $currentValue Current duration value (null if not set) * @param string $prompt Prompt text for user input * @param int|null $existingValue Existing duration from current settings - * @param int $defaultValue Default duration value + * @param int|null $defaultValue Default duration value * @param string $service Service name * @param string $updateKey Key to use in updates array * @param array> $updates Updates array (passed by reference) @@ -490,7 +494,7 @@ private function askForDurationSetting( ?int $currentValue, string $prompt, ?int $existingValue, - int $defaultValue, + ?int $defaultValue, string $service, string $updateKey, array &$updates @@ -498,8 +502,8 @@ private function askForDurationSetting( if ($currentValue === null) { $choices = array_keys(self::$validDurations); $default = $existingValue ?? $defaultValue; - $newValue = $this->questionHelper->askInput($prompt, $this->formatDuration($default), $choices, function ($v) { - return $this->validateDuration($v); + $newValue = $this->questionHelper->askInput($prompt, $default !== null ? $this->formatDuration($default) : null, $choices, function (?string $v) { + return $this->validateDuration($this->requireAnswer($v)); }); $this->stdErr->writeln(''); @@ -585,7 +589,7 @@ protected function makeAutoscalingSettingsData(array $updates): array * Summarizes all the changes that would be made. * * @param array> $updates - * @param array> $settings + * @param array $settings */ private function summarizeChanges(array $updates, array $settings): void { @@ -598,14 +602,17 @@ private function summarizeChanges(array $updates, array $settings): void /** * Summarizes changes per service. * - * @param array|null $current + * @param ServiceSettings|null $current * @param array $updates */ private function summarizeChangesPerService(string $name, ?array $current, array $updates): void { $this->stdErr->writeln(sprintf(' Service: %s', $name)); - $metric = $updates['metric']; + $metric = $updates['metric'] ?? null; + if (!is_string($metric)) { + throw new \LogicException('No metric is set for the service: ' . $name); + } $this->stdErr->writeln(sprintf(' Metric: %s', $metric)); $action = 'remain'; @@ -626,20 +633,20 @@ private function summarizeChangesPerService(string $name, ?array $current, array if (isset($updates['threshold-up'])) { $this->stdErr->writeln(' Threshold: ' . $this->resourcesUtil->formatChange( - isset($current['triggers'][$metric]['up']) ? $current['triggers'][$metric]['up']['threshold'] : null, + $current['triggers'][$metric]['up']['threshold'] ?? null, $updates['threshold-up'], '%' )); } if (isset($updates['duration-up'])) { $this->stdErr->writeln(' Duration: ' . $this->formatDurationChange( - isset($current['triggers'][$metric]['up']) ? $this->formatDuration($current['triggers'][$metric]['up']['duration']) : null, + isset($current['triggers'][$metric]['up']['duration']) ? $this->formatDuration($current['triggers'][$metric]['up']['duration']) : null, $this->formatDuration($updates['duration-up']) )); } if (isset($updates['cooldown-up'])) { $this->stdErr->writeln(' Cooldown: ' . $this->formatDurationChange( - isset($current['scale_cooldown']) ? $this->formatDuration($current['scale_cooldown']['up']) : null, + isset($current['scale_cooldown']['up']) ? $this->formatDuration($current['scale_cooldown']['up']) : null, $this->formatDuration($updates['cooldown-up']) )); } @@ -650,20 +657,20 @@ private function summarizeChangesPerService(string $name, ?array $current, array if (isset($updates['threshold-down'])) { $this->stdErr->writeln(' Threshold: ' . $this->resourcesUtil->formatChange( - isset($current['triggers'][$metric]['down']) ? $current['triggers'][$metric]['down']['threshold'] : null, + $current['triggers'][$metric]['down']['threshold'] ?? null, $updates['threshold-down'], '%' )); } if (isset($updates['duration-down'])) { $this->stdErr->writeln(' Duration: ' . $this->formatDurationChange( - isset($current['triggers'][$metric]['down']) ? $this->formatDuration($current['triggers'][$metric]['down']['duration']) : null, + isset($current['triggers'][$metric]['down']['duration']) ? $this->formatDuration($current['triggers'][$metric]['down']['duration']) : null, $this->formatDuration($updates['duration-down']) )); } if (isset($updates['cooldown-down'])) { $this->stdErr->writeln(' Cooldown: ' . $this->formatDurationChange( - isset($current['scale_cooldown']) ? $this->formatDuration($current['scale_cooldown']['down']) : null, + isset($current['scale_cooldown']['down']) ? $this->formatDuration($current['scale_cooldown']['down']) : null, $this->formatDuration($updates['cooldown-down']) )); } @@ -673,14 +680,14 @@ private function summarizeChangesPerService(string $name, ?array $current, array $this->stdErr->writeln(' Instances'); if (isset($updates['instances-min'])) { $this->stdErr->writeln(' Min: ' . $this->resourcesUtil->formatChange( - isset($current['instances']) ? $current['instances']['min'] : null, + $current['instances']['min'] ?? null, $updates['instances-min'] )); } if (isset($updates['instances-max'])) { $this->stdErr->writeln(' Max: ' . $this->resourcesUtil->formatChange( - isset($current['instances']) ? $current['instances']['max'] : null, + $current['instances']['max'] ?? null, $updates['instances-max'] )); } @@ -786,7 +793,7 @@ protected function filterServicesWithAutoscalingSupport(array $services, \Platfo /** * Return the names of supported metrics. * - * @param array $defaults Autoscaling settings defaults + * @param ServiceSettings $defaults Autoscaling settings defaults * * @return array Supported metric names */ @@ -838,6 +845,19 @@ protected function validateBoolean(float|int|string|bool $value): bool }; } + /** + * Rejects an empty answer to a question without a default, which Symfony passes as null. + * + * @throws InvalidArgumentException + */ + private function requireAnswer(?string $value): string + { + if ($value === null) { + throw new InvalidArgumentException('A value is required.'); + } + return $value; + } + /** * Validates a given threshold. * @@ -979,7 +999,7 @@ protected function formatDurationChange(int|string|null $previousValue, int|stri $previousValue, $newValue, '', - function ($previousValue, $newValue) { + function (string $previousValue, string $newValue): bool { if (!isset(self::$validDurations[$previousValue]) || !isset(self::$validDurations[$newValue])) { throw new InvalidArgumentException(sprintf( 'Invalid duration key(s): previousValue=%s, newValue=%s. Valid keys are: %s', diff --git a/legacy/src/Command/Environment/EnvironmentPushCommand.php b/legacy/src/Command/Environment/EnvironmentPushCommand.php index f9655d473..c38faf424 100644 --- a/legacy/src/Command/Environment/EnvironmentPushCommand.php +++ b/legacy/src/Command/Environment/EnvironmentPushCommand.php @@ -338,9 +338,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Check the push log for other possible deployment error messages. - $messages = (array) $this->config->get('detection.push_deploy_error_messages'); - foreach ($messages as $message) { - if (str_contains($log, (string) $message)) { + foreach ($this->config->getStrList('detection.push_deploy_error_messages') as $message) { + if (str_contains($log, $message)) { $this->stdErr->writeln(''); $this->stdErr->writeln(\sprintf('The push completed but there was a deployment error ("%s").', $message)); diff --git a/legacy/src/Command/Organization/OrganizationCreateCommand.php b/legacy/src/Command/Organization/OrganizationCreateCommand.php index 5a6e07321..5cc52c4a0 100644 --- a/legacy/src/Command/Organization/OrganizationCreateCommand.php +++ b/legacy/src/Command/Organization/OrganizationCreateCommand.php @@ -47,16 +47,16 @@ private function getForm(): Form $fields['label'] = new Field('Label', [ 'description' => 'The full name of the organization, e.g. "ACME Inc."', ]); - if ($orgTypes = $this->config->get('api.organization_types')) { + if ($orgTypes = $this->config->getStrList('api.organization_types')) { $options = []; - foreach ((array) $orgTypes as $type) { + foreach ($orgTypes as $type) { $options[$type] = ucfirst($type); } $fields['type'] = new OptionsField('Type', [ 'description' => 'The organization type.', 'options' => $options, 'avoidQuestion' => true, - 'default' => $this->config->getWithDefault('api.default_organization_type', key($options)), + 'default' => $this->config->getStrOrNull('api.default_organization_type') ?? key($options), ]); } $fields['name'] = new Field('Name', [ diff --git a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php index db8bfd1c5..b564fcdf6 100644 --- a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php +++ b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php @@ -48,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $project = $selection->getProject(); - $settings = $project->getSettings(); + $current = $this->resourcesUtil->buildResources($project->getSettings()); $isOriginalCommand = $input instanceof ArgvInput; @@ -58,8 +58,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $rows = [ [ - 'cpu' => $settings['build_resources']['cpu'], - 'memory' => $settings['build_resources']['memory'], + 'cpu' => $current['cpu'] ?? '', + 'memory' => $current['memory'] ?? '', ], ]; diff --git a/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php index 868d9d7b5..c7f24c505 100644 --- a/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php +++ b/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php @@ -46,12 +46,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $capabilities = $this->api->getProjectCapabilities($project); $capability = $capabilities->getProperty('build_resources', false); - $maxCpu = $capability ? $capability['max_cpu'] : null; - $maxMemory = $capability ? $capability['max_memory'] : null; + $maxCpu = is_array($capability) ? $capability['max_cpu'] ?? null : null; + $maxCpu = is_int($maxCpu) || is_float($maxCpu) ? (float) $maxCpu : null; + $maxMemory = is_array($capability) ? $capability['max_memory'] ?? null : null; + $maxMemory = is_int($maxMemory) ? $maxMemory : null; $settings = $project->getSettings(); + $current = $this->resourcesUtil->buildResources($settings); - $validateCpu = function ($v) use ($maxCpu): float { + $validateCpu = function (string $v) use ($maxCpu): float { $f = (float) $v; if ($f != $v) { throw new InvalidArgumentException('The CPU value must be a number'); @@ -59,12 +62,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($f < 0.1) { throw new InvalidArgumentException(sprintf('The minimum allowed CPU is %.1f', 0.1)); } - if ($f > $maxCpu) { + if ($maxCpu !== null && $f > $maxCpu) { throw new InvalidArgumentException(sprintf('The maximum allowed CPU is %.1f', $maxCpu)); } return $f; }; - $validateMemory = function ($v) use ($maxMemory): int { + $validateMemory = function (string $v) use ($maxMemory): int { $i = (int) $v; if ($i != $v) { throw new InvalidArgumentException('The memory value must be an integer'); @@ -72,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($i < 64) { throw new InvalidArgumentException(sprintf('The minimum allowed memory is %d MB', 64)); } - if ($i > $maxMemory) { + if ($maxMemory !== null && $i > $maxMemory) { throw new InvalidArgumentException(sprintf('The maximum allowed memory is %d MB', $maxMemory)); } return $i; @@ -101,7 +104,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($cpuOption === null && $memoryOption === null) { $cpuOption = $this->questionHelper->askInput( 'CPU size', - $this->resourcesUtil->formatCPU($settings['build_resources']['cpu']), + $current['cpu'] !== null ? $this->resourcesUtil->formatCPU($current['cpu']) : null, [], $validateCpu, 'current: ', @@ -109,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $memoryOption = $this->questionHelper->askInput( 'Memory size in MB', - $settings['build_resources']['memory'], + $current['memory'], [], $validateMemory, 'current: ', @@ -118,10 +121,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $updates = []; - if ($cpuOption !== null && $cpuOption !== $settings['build_resources']['cpu']) { + if ($cpuOption !== null && $cpuOption !== $current['cpu']) { $updates['build_resources']['cpu'] = $cpuOption; } - if ($memoryOption !== null && $memoryOption !== $settings['build_resources']['memory']) { + if ($memoryOption !== null && $memoryOption !== $current['memory']) { $updates['build_resources']['memory'] = $memoryOption; } @@ -130,7 +133,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - $this->summarizeChanges($updates['build_resources'], $settings['build_resources']); + $this->summarizeChanges($updates['build_resources'], $current); $this->io->debug('Raw updates: ' . json_encode($updates, JSON_UNESCAPED_SLASHES)); @@ -150,21 +153,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Summarizes all the changes that would be made. * - * @param array{cpu?: int, memory?: int} $updates - * @param array{cpu: int, memory: int} $current + * @param array{cpu?: float, memory?: int} $updates + * @param array{cpu: float|null, memory: int|null} $current * @return void */ private function summarizeChanges(array $updates, array $current): void { $this->stdErr->writeln('Summary of changes:'); - $this->stdErr->writeln(' CPU: ' . $this->resourcesUtil->formatChange( - $this->resourcesUtil->formatCPU($current['cpu']), - $this->resourcesUtil->formatCPU($updates['cpu'] ?? $current['cpu']), - )); - $this->stdErr->writeln(' Memory: ' . $this->resourcesUtil->formatChange( - $current['memory'], - $updates['memory'] ?? $current['memory'], - ' MB', - )); + $cpu = $updates['cpu'] ?? $current['cpu']; + if ($cpu !== null) { + $this->stdErr->writeln(' CPU: ' . $this->resourcesUtil->formatChange( + $current['cpu'] !== null ? $this->resourcesUtil->formatCPU($current['cpu']) : null, + $this->resourcesUtil->formatCPU($cpu), + )); + } + $memory = $updates['memory'] ?? $current['memory']; + if ($memory !== null) { + $this->stdErr->writeln(' Memory: ' . $this->resourcesUtil->formatChange($current['memory'], $memory, ' MB')); + } } } diff --git a/legacy/src/Command/Resources/ResourcesCommandBase.php b/legacy/src/Command/Resources/ResourcesCommandBase.php index db10c5665..99444c20f 100644 --- a/legacy/src/Command/Resources/ResourcesCommandBase.php +++ b/legacy/src/Command/Resources/ResourcesCommandBase.php @@ -5,15 +5,14 @@ namespace Platformsh\Cli\Command\Resources; use Platformsh\Cli\Command\CommandBase; -use Platformsh\Cli\Console\ArrayArgument; use Platformsh\Cli\Service\Config; -use Platformsh\Cli\Util\Wildcard; -use Platformsh\Client\Model\Deployment\Service; -use Platformsh\Client\Model\Deployment\WebApp; -use Platformsh\Client\Model\Deployment\Worker; -use Symfony\Component\Console\Input\InputInterface; +use Platformsh\Cli\Service\ResourcesUtil; use Symfony\Contracts\Service\Attribute\Required; +/** + * @phpstan-import-type ProfileSize from ResourcesUtil + * @phpstan-import-type ContainerProfiles from ResourcesUtil + */ class ResourcesCommandBase extends CommandBase { private Config $config; @@ -29,182 +28,27 @@ public function isHidden(): bool return !$this->config->getBool('api.sizing') || parent::isHidden(); } - /** - * Checks whether a service needs a persistent disk. - * - * @param WebApp|Service|Worker $service - * @return bool - */ - protected function supportsDisk($service) - { - // Workers use the disk of their parent app. - if ($service instanceof Worker) { - return false; - } - return isset($service->getProperties()['resources']['minimum']['disk']); - } - - /** - * Filters a list of services according to the --service or --type options. - * - * @param array $services - * @param InputInterface $input - * - * @return WebApp[]|Service[]|Worker[]|false - * False on error, or an array of services. - */ - protected function filterServices($services, InputInterface $input) - { - $selectedNames = []; - - $requestedServices = ArrayArgument::getOption($input, 'service'); - if (!empty($requestedServices)) { - $selectedNames = Wildcard::select(array_keys($services), $requestedServices); - if (!$selectedNames) { - $this->stdErr->writeln('No services were found matching the name(s): ' . implode(', ', $requestedServices) . ''); - return false; - } - $services = array_intersect_key($services, array_flip($selectedNames)); - } - $requestedApps = ArrayArgument::getOption($input, 'app'); - if (!empty($requestedApps)) { - $selectedNames = Wildcard::select(array_keys(array_filter($services, function ($s) { - return $s instanceof WebApp; - })), $requestedApps); - if (!$selectedNames) { - $this->stdErr->writeln('No applications were found matching the name(s): ' . implode(', ', $requestedApps) . ''); - return false; - } - $services = array_intersect_key($services, array_flip($selectedNames)); - } - $requestedWorkers = ArrayArgument::getOption($input, 'worker'); - if (!empty($requestedWorkers)) { - $selectedNames = Wildcard::select(array_keys(array_filter($services, function ($s) { - return $s instanceof Worker; - })), $requestedWorkers); - if (!$selectedNames) { - $this->stdErr->writeln('No workers were found matching the name(s): ' . implode(', ', $requestedWorkers) . ''); - return false; - } - $services = array_intersect_key($services, array_flip($selectedNames)); - } - - if ($input->hasOption('type') && ($requestedTypes = ArrayArgument::getOption($input, 'type'))) { - $byType = []; - foreach ($services as $name => $service) { - $type = $service->type; - [$prefix] = explode(':', $service->type, 2); - $byType[$type][] = $name; - $byType[$prefix][] = $name; - } - $selectedTypes = Wildcard::select(array_keys($byType), $requestedTypes); - if (!$selectedTypes) { - $this->stdErr->writeln('No services were found matching the type(s): ' . implode(', ', $requestedTypes) . ''); - return false; - } - foreach ($selectedTypes as $selectedType) { - $selectedNames = array_merge($selectedNames, $byType[$selectedType]); - } - $services = array_intersect_key($services, array_flip($selectedNames)); - } - - return $services; - } - - /** - * Returns container profile size info, given service properties. - * - * @param array $properties - * The service properties (e.g. from $service->getProperties()). - * @param array>> $containerProfiles - * The list of container profiles (e.g. from - * $deployment->container_profiles). - * - * @return array|null - */ - protected function sizeInfo(array $properties, array $containerProfiles) - { - if (isset($properties['resources']['profile_size'])) { - $size = $properties['resources']['profile_size']; - $profile = $properties['container_profile']; - if (isset($containerProfiles[$profile][$size])) { - return $containerProfiles[$profile][$size]; - } - } - return null; - } - - /** - * Formats a change in a value. - * - * @param int|float|string|null $previousValue - * @param int|float|string|null $newValue - * @param string $suffix A unit suffix e.g. ' MB' - * - * @return string - */ - protected function formatChange($previousValue, $newValue, $suffix = '') - { - if ($previousValue === null || $newValue === $previousValue) { - return sprintf('%s%s', $newValue, $suffix); - } - return sprintf( - '%s from %s%s to %s%s', - $newValue > $previousValue ? 'increasing' : 'decreasing', - $previousValue, - $suffix, - $newValue, - $suffix - ); - } - - /** - * Formats a CPU amount. - * - * @param int|float|string $unformatted - * - * @return string - * A numeric (still comparable) string with 1 decimal place. - */ - protected function formatCPU($unformatted) - { - return sprintf('%.1f', $unformatted); - } - /** * Format CPU Type. * - * @param array|null $sizeInfo - * - * @return string + * @param ProfileSize $sizeInfo */ - protected function formatCPUType($sizeInfo) + protected function formatCPUType(array $sizeInfo): string { - $size = $sizeInfo ? $sizeInfo['cpu'] : null; - if ($size === null) { - return ""; - } - - if (!isset($sizeInfo['cpu_type'])) { - return ""; - } - return sprintf('(%s)', $sizeInfo['cpu_type']); } /** * Sort container profiles by size. * - * @param array>> $profiles + * @param ContainerProfiles $profiles * - * @return array>> + * @return ContainerProfiles */ - protected function sortContainerProfiles(array $profiles) + protected function sortContainerProfiles(array $profiles): array { foreach ($profiles as &$profile) { - uasort($profile, function ($a, $b) { - return $a['cpu'] == $b['cpu'] ? 0 : ($a['cpu'] > $b['cpu'] ? 1 : -1); - }); + uasort($profile, fn(array $a, array $b): int => $a['cpu'] <=> $b['cpu']); } return $profiles; diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index cf825206b..0ec9526f0 100644 --- a/legacy/src/Command/Resources/ResourcesGetCommand.php +++ b/legacy/src/Command/Resources/ResourcesGetCommand.php @@ -100,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Check autoscaling settings for the environment, as autoscaling prevents changing some resources manually. $autoscalingSettings = $this->api->getAutoscalingSettings($environment); if ($autoscalingSettings) { - foreach ($autoscalingSettings->getData()['services'] as $service => $serviceSettings) { + foreach ($autoscalingSettings->services as $service => $serviceSettings) { $autoscalingEnabled[$service] = !empty($serviceSettings['enabled']); } } @@ -112,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $empty = $this->table->formatIsMachineReadable() ? '' : 'not set'; $notApplicable = $this->table->formatIsMachineReadable() ? '' : 'N/A'; - $containerProfiles = $this->sortContainerProfiles($nextDeployment->container_profiles); + $containerProfiles = $this->sortContainerProfiles($this->resourcesUtil->containerProfiles($nextDeployment)); $rows = []; $cpuTypeOption = Option::stringOrNull($input, 'cpu-type'); @@ -121,8 +121,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $hasObjectStorage = false; foreach ($services as $name => $service) { $properties = $service->getProperties(); - // Tasks may not specify a profile yet; default so CPU/memory still resolve. - $containerProfile = $properties['container_profile'] ?? ($service instanceof Task ? 'BALANCED' : null); + $resources = $this->resourcesUtil->resources($service); + $containerProfile = $this->resourcesUtil->containerProfile($service); if (!$this->table->formatIsMachineReadable() && !empty($autoscalingEnabled[$name])) { $name .= ' ' . $autoscalingIndicator; $hasAutoscalingIndicator = true; @@ -143,22 +143,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'memory' => $empty, ]; - $profileSize = $properties['resources']['profile_size'] ?? null; + $profileSize = $resources['profile_size'] ?? null; if ($containerProfile !== null && $profileSize !== null && isset($containerProfiles[$containerProfile][$profileSize])) { $profileInfo = $containerProfiles[$containerProfile][$profileSize]; - if ($cpuTypeOption != "" && isset($profileInfo['cpu_type']) && $profileInfo['cpu_type'] != $cpuTypeOption) { + if ($cpuTypeOption != "" && $profileInfo['cpu_type'] != $cpuTypeOption) { continue; } - $row['cpu_type'] = $profileInfo['cpu_type'] ?? ''; - $row['cpu'] = isset($profileInfo['cpu']) ? $this->resourcesUtil->formatCPU($profileInfo['cpu']) : ''; - $row['memory'] = isset($profileInfo['cpu']) ? $profileInfo['memory'] : ''; + $row['cpu_type'] = $profileInfo['cpu_type']; + $row['cpu'] = $this->resourcesUtil->formatCPU($profileInfo['cpu']); + $row['memory'] = $profileInfo['memory']; } - if (!empty($properties['resources'])) { - foreach ($properties['resources'] as $key => $value) { - if (isset($row[$key])) { - $row[$key] = $value === null ? '' : $value; + if (isset($properties['resources']) && is_array($properties['resources'])) { + foreach (['profile_size', 'base_memory', 'memory_ratio'] as $key) { + if (array_key_exists($key, $properties['resources'])) { + $row[$key] = $resources[$key] ?? ''; } } } @@ -166,19 +166,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$this->resourcesUtil->supportsDisk($service)) { $row['disk'] = $notApplicable; } elseif (array_key_exists('disk', $properties)) { - if (empty($properties['disk'])) { + $disk = $this->resourcesUtil->disk($service); + if (empty($disk)) { $row['disk'] = empty($properties['resources']) ? $empty : ''; } else { - $row['disk'] = $this->propertyFormatter->format($properties['disk'], 'disk'); + $row['disk'] = $this->propertyFormatter->format($disk, 'disk'); } } // Object storage is only available on apps. if (!$service instanceof WebApp) { $row['object_storage'] = $notApplicable; - } elseif (isset($properties['resources']['disk']['object'])) { - $row['object_storage'] = (string) $properties['resources']['disk']['object']; - if ($properties['resources']['disk']['object'] > 0) { + } elseif (isset($resources['object_storage'])) { + $row['object_storage'] = (string) $resources['object_storage']; + if ($resources['object_storage'] > 0) { $hasObjectStorage = true; } } @@ -187,7 +188,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($service instanceof Task) { $row['instance_count'] = $notApplicable; } else { - $row['instance_count'] = isset($properties['instance_count']) ? $this->propertyFormatter->format($properties['instance_count'], 'instance_count') : '1'; + $instanceCount = $this->resourcesUtil->instanceCount($service); + $row['instance_count'] = $instanceCount !== null ? $this->propertyFormatter->format($instanceCount, 'instance_count') : '1'; } $rows[] = $row; diff --git a/legacy/src/Command/Resources/ResourcesSetCommand.php b/legacy/src/Command/Resources/ResourcesSetCommand.php index ec502f81d..45438505b 100644 --- a/legacy/src/Command/Resources/ResourcesSetCommand.php +++ b/legacy/src/Command/Resources/ResourcesSetCommand.php @@ -30,6 +30,12 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @phpstan-import-type ContainerProfiles from ResourcesUtil + * @phpstan-import-type ProfileSize from ResourcesUtil + * @phpstan-type ServiceUpdates array{resources?: array{profile_size?: string, disk?: array{object: int}}, instance_count?: int, disk?: int} + * @phpstan-type CurrentService array{profile_size: string|null, instance_count: int|null, disk: int|null, sizes: array} + */ #[AsCommand(name: 'resources:set', description: 'Set the resources of apps, tasks and services on an environment')] class ResourcesSetCommand extends ResourcesCommandBase { @@ -144,32 +150,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Determine the limit of the number of instances, which can vary per project. $instanceLimit = null; - if (($projectInfo = $nextDeployment->getProperty('project_info')) && isset($projectInfo['capabilities']['instance_limit'])) { - $instanceLimit = $projectInfo['capabilities']['instance_limit']; + $projectInfo = $nextDeployment->getProperty('project_info', false); + if (is_array($projectInfo) && isset($projectInfo['capabilities']) && is_array($projectInfo['capabilities'])) { + $limit = $projectInfo['capabilities']['instance_limit'] ?? null; + $instanceLimit = is_int($limit) ? $limit : null; } $autoscalingEnabled = []; // Check autoscaling settings for the environment, as autoscaling prevents changing some resources manually. $autoscalingSettings = $this->api->getAutoscalingSettings($environment); if ($autoscalingSettings) { - foreach ($autoscalingSettings->getData()['services'] as $service => $serviceSettings) { + foreach ($autoscalingSettings->services as $service => $serviceSettings) { $autoscalingEnabled[$service] = !empty($serviceSettings['enabled']); } } // Validate the --size option. - [$givenSizes, $errored] = $this->parseSetting($input, 'size', $services, fn($v, $serviceName, $service) => $this->validateProfileSize($v, $serviceName, $service, $nextDeployment)); + [$givenSizes, $errored] = $this->parseSetting($input, 'size', $services, fn(string $v, string $serviceName, WebApp|Worker|Service|Task $service): string => $this->validateProfileSize($v, $serviceName, $service, $nextDeployment)); // Validate the --count option. - [$givenCounts, $countErrored] = $this->parseSetting($input, 'count', $services, fn($v, $serviceName, $service) => $this->validateInstanceCount($v, $serviceName, $service, $instanceLimit, !empty($autoscalingEnabled[$serviceName]))); + [$givenCounts, $countErrored] = $this->parseSetting($input, 'count', $services, fn(string $v, string $serviceName, WebApp|Worker|Service|Task $service): int => $this->validateInstanceCount($v, $serviceName, $service, $instanceLimit, !empty($autoscalingEnabled[$serviceName]))); $errored = $errored || $countErrored; // Validate the --disk option. - [$givenDiskSizes, $diskErrored] = $this->parseSetting($input, 'disk', $services, fn($v, $serviceName, $service) => $this->validateDiskSize($v, $serviceName, $service)); + [$givenDiskSizes, $diskErrored] = $this->parseSetting($input, 'disk', $services, fn(string $v, string $serviceName, WebApp|Worker|Service|Task $service): int => $this->validateDiskSize($v, $serviceName, $service)); $errored = $errored || $diskErrored; // Validate the --object-storage option. - [$givenObjectStorage, $objectStorageErrored] = $this->parseSetting($input, 'object-storage', $services, fn($v, $serviceName, $service) => $this->validateObjectStorage($v, $serviceName, $service)); + [$givenObjectStorage, $objectStorageErrored] = $this->parseSetting($input, 'object-storage', $services, fn(string $v, string $serviceName, WebApp|Worker|Service|Task $service): int => $this->validateObjectStorage($v, $serviceName, $service)); $errored = $errored || $objectStorageErrored; if ($errored) { return 1; @@ -183,7 +191,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->stdErr->writeln(''); - $containerProfiles = $nextDeployment->container_profiles; + $allContainerProfiles = $this->resourcesUtil->containerProfiles($nextDeployment); + $containerProfiles = $allContainerProfiles; // Remove guaranteed profiles if project does not support it. $supportsGuaranteedCPU = $this->api->supportsGuaranteedCPU($selection->getProject(), $nextDeployment); @@ -209,14 +218,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $type = $this->typeName($service); $group = $this->group($service); - $properties = $service->getProperties(); - // Tasks may lack a profile, instance_count and disk; default safely. - $containerProfile = $properties['container_profile'] ?? ($service instanceof Task ? 'BALANCED' : null); - $current[$group][$name]['resources']['profile_size'] = $properties['resources']['profile_size'] ?? null; - $current[$group][$name]['instance_count'] = $properties['instance_count'] ?? null; - $current[$group][$name]['disk'] = $properties['disk'] ?? null; - // Unfiltered, so that usage is also known for a current size that is no longer offered. - $current[$group][$name]['sizes'] = $nextDeployment->container_profiles[$containerProfile] ?? []; + $resources = $this->resourcesUtil->resources($service); + $currentSize = $resources['profile_size'] ?? null; + $currentDisk = $this->resourcesUtil->disk($service); + $containerProfile = $this->resourcesUtil->containerProfile($service); + $current[$group][$name] = [ + 'profile_size' => $currentSize, + 'instance_count' => $this->resourcesUtil->instanceCount($service), + 'disk' => $currentDisk, + // Unfiltered, so that usage is also known for a current size that is no longer offered. + 'sizes' => $allContainerProfiles[$containerProfile] ?? [], + ]; $header = '' . ucfirst($type) . ': ' . $name . ''; $headerShown = false; @@ -230,34 +242,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Set the profile size. if (isset($givenSizes[$name])) { - if (!isset($properties['resources']['profile_size']) || $givenSizes[$name] != $properties['resources']['profile_size']) { + if ($currentSize === null || $givenSizes[$name] != $currentSize) { $updates[$group][$name]['resources']['profile_size'] = $givenSizes[$name]; } } elseif ($showCompleteForm - || (!isset($properties['resources']['profile_size']) && $input->isInteractive())) { + || ($currentSize === null && $input->isInteractive())) { if ($containerProfile !== null) { $header .= "\n" . sprintf('Container profile: %s', $containerProfile); } $ensureHeader(); - $new = isset($properties['resources']['profile_size']) ? 'a new' : 'a'; + $new = $currentSize !== null ? 'a new' : 'a'; $profileSizes = $containerProfiles[$containerProfile] ?? []; - if (isset($properties['resources']['profile_size'])) { - $defaultOption = $properties['resources']['profile_size']; - } elseif (isset($properties['resources']['default']['profile_size'])) { - $defaultOption = $properties['resources']['default']['profile_size']; - } else { - $defaultOption = null; - } + $defaultOption = $currentSize ?? $resources['default']['profile_size'] ?? null; $options = []; foreach ($profileSizes as $profileSize => $sizeInfo) { // Skip showing sizes that are below the minimum for this service. - if ((isset($properties['resources']['minimum']['cpu']) && $sizeInfo['cpu'] < $properties['resources']['minimum']['cpu']) - || (isset($properties['resources']['minimum']['memory']) && $sizeInfo['memory'] < $properties['resources']['minimum']['memory'])) { + if ((isset($resources['minimum']['cpu']) && $sizeInfo['cpu'] < $resources['minimum']['cpu']) + || (isset($resources['minimum']['memory']) && $sizeInfo['memory'] < $resources['minimum']['memory'])) { continue; } $description = sprintf('CPU %s, memory %s MB (%s)', $sizeInfo['cpu'], $sizeInfo['memory'], $sizeInfo['cpu_type']); - if (isset($properties['resources']['profile_size']) - && $profileSize == $properties['resources']['profile_size']) { + if ($currentSize !== null && $profileSize == $currentSize) { $description .= ' (current)'; } elseif ($defaultOption !== null && $defaultOption === (string) $profileSize) { $description .= ' (default)'; @@ -266,16 +271,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $options[$profileSize] = $description; } - if (!isset($properties['resources']['profile_size']) && empty($options)) { + if ($currentSize === null && empty($options)) { $this->stdErr->writeln(sprintf('No profile size can be found for the %s %s which satisfies its minimum resources.', $type, $name)); $errored = true; } else { $profileSize = $this->questionHelper->chooseAssoc($options, sprintf('Choose %s profile size:', $new), $defaultOption, false, false); - if (!isset($properties['resources']['profile_size']) || $profileSize !== $properties['resources']['profile_size']) { + if ($profileSize !== $currentSize) { $updates[$group][$name]['resources']['profile_size'] = $profileSize; } } - } elseif (!isset($properties['resources']['profile_size'])) { + } elseif ($currentSize === null) { $this->stdErr->writeln(sprintf('A profile size is required for the %s %s.', $type, $name)); $errored = true; } @@ -294,7 +299,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // This is not applicable to a Task or a non-scalable Service, and unavailable when autoscaling is enabled. if ($this->supportsInstanceCount($service) && empty($autoscalingEnabled[$name])) { // An unset instance count means 1. - $currentCount = $properties['instance_count'] ?? 1; + $currentCount = $current[$group][$name]['instance_count'] ?? 1; if (isset($givenCounts[$name])) { $instanceCount = $givenCounts[$name]; if ($instanceCount !== $currentCount) { @@ -317,22 +322,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Set the disk size. if ($this->resourcesUtil->supportsDisk($service)) { + $diskIsRequiredButUnset = $this->diskIsRequiredButUnset($service); if (isset($givenDiskSizes[$name])) { - if ($givenDiskSizes[$name] !== ($properties['disk'] ?? null)) { + if ($givenDiskSizes[$name] !== $currentDisk) { $updates[$group][$name]['disk'] = $givenDiskSizes[$name]; } - } elseif ($showCompleteForm || ($this->diskIsRequiredButUnset($properties) && $input->isInteractive())) { + } elseif ($showCompleteForm || ($diskIsRequiredButUnset && $input->isInteractive())) { $ensureHeader(); - if (($properties['disk'] ?? null)) { - $default = $properties['disk']; - } else { - $default = $properties['resources']['default']['disk'] ?? '512'; - } + $default = $currentDisk ?: $resources['default']['disk'] ?? 512; $diskSize = $this->questionHelper->askInput('Enter a disk size in MB', (string) $default, ['512', '1024', '2048'], fn($v) => $this->validateDiskSize($v, $name, $service)); - if ($diskSize !== ($properties['disk'] ?? null)) { + if ($diskSize !== $currentDisk) { $updates[$group][$name]['disk'] = $diskSize; } - } elseif ($this->diskIsRequiredButUnset($properties)) { + } elseif ($diskIsRequiredButUnset) { $this->stdErr->writeln(sprintf('A disk size is required for the %s %s.', $type, $name)); $errored = true; } @@ -340,7 +342,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Set the object storage size (apps only). if ($service instanceof WebApp && isset($givenObjectStorage[$name])) { - $currentObject = $properties['resources']['disk']['object'] ?? null; + $currentObject = $resources['object_storage'] ?? null; if ($givenObjectStorage[$name] !== $currentObject) { $updates[$group][$name]['resources']['disk']['object'] = $givenObjectStorage[$name]; } @@ -437,9 +439,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 ContainerProfiles $containerProfiles * @return void */ private function summarizeChanges(array $updates, array $services, array $containerProfiles): void @@ -455,20 +457,18 @@ private function summarizeChanges(array $updates, array $services, array $contai /** * Summarizes changes per service. * - * @param array $updates - * @param array $containerProfiles + * @param ServiceUpdates $updates + * @param ContainerProfiles $containerProfiles */ private function summarizeChangesPerService(string $name, WebApp|Worker|Service|Task $service, array $updates, array $containerProfiles): void { $this->stdErr->writeln(sprintf(' %s: %s', ucfirst($this->typeName($service)), $name)); - $properties = $service->getProperties(); - $properties['container_profile'] ??= $service instanceof Task ? 'BALANCED' : null; + $resources = $this->resourcesUtil->resources($service); if (isset($updates['resources']['profile_size'])) { - $sizeInfo = $this->resourcesUtil->sizeInfo($properties, $containerProfiles); - $newProperties = array_replace_recursive($properties, $updates); - - $newSizeInfo = $this->resourcesUtil->sizeInfo($newProperties, $containerProfiles); + $sizes = $containerProfiles[$this->resourcesUtil->containerProfile($service)] ?? []; + $sizeInfo = isset($resources['profile_size']) ? $sizes[$resources['profile_size']] ?? null : null; + $newSizeInfo = $sizes[$updates['resources']['profile_size']] ?? null; if ($newSizeInfo === null) { // The requested profile_size isn't in the deployment's // container_profiles catalog, so we can't show CPU/memory @@ -492,20 +492,20 @@ private function summarizeChangesPerService(string $name, WebApp|Worker|Service| } if (isset($updates['instance_count'])) { $this->stdErr->writeln(' Instance count: ' . $this->resourcesUtil->formatChange( - $properties['instance_count'] ?? 1, + $this->resourcesUtil->instanceCount($service) ?? 1, $updates['instance_count'], )); } if (isset($updates['disk'])) { $this->stdErr->writeln(' Disk: ' . $this->resourcesUtil->formatChange( - $properties['disk'] ?? null, + $this->resourcesUtil->disk($service), $updates['disk'], ' MB', )); } if (isset($updates['resources']['disk']['object'])) { $this->stdErr->writeln(' Object storage: ' . $this->resourcesUtil->formatChange( - $properties['resources']['disk']['object'] ?? null, + $resources['object_storage'] ?? null, $updates['resources']['disk']['object'], ' MB', )); @@ -593,13 +593,11 @@ protected function validateInstanceCount(string $value, string $serviceName, Web * * A zero minimum means a disk is allowed but optional, as on Upsun, so * leaving it unset is a valid final state rather than something to fix. - * - * @param array $properties */ - protected function diskIsRequiredButUnset(array $properties): bool + protected function diskIsRequiredButUnset(WebApp|Worker|Service|Task $service): bool { - return !empty($properties['resources']['minimum']['disk']) - && empty($properties['disk'] ?? null); + return !empty($this->resourcesUtil->resources($service)['minimum']['disk']) + && empty($this->resourcesUtil->disk($service)); } /** @@ -727,15 +725,17 @@ protected function validateProfileSize(string $value, string $serviceName, WebAp /** * Parses a user's list of settings for --size, --instances or --disk. * + * @template T of int|string + * * @param InputInterface $input * @param string $optionName The input option name. * @param array $services - * @param callable $validator + * @param callable(string, string, Service|WebApp|Worker|Task): T $validator * Validate the value. The callback takes the arguments ($value, * $serviceName, $service) and returns a normalized value or throws * an InvalidArgumentException. * - * @return array{array, bool} + * @return array{array, bool} * An array of settings per service, and whether an error occurred. */ private function parseSetting(InputInterface $input, string $optionName, array $services, callable $validator): array @@ -845,8 +845,8 @@ private function trialResourceLimits(Project $project): 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{memory: int|float, cpu: int|float, disk: int|float} */ @@ -859,16 +859,10 @@ private function computeMemoryCPUStorageDiff(array $updates, array $current): ar ]; foreach ($updates as $group => $groupUpdates) { foreach ($groupUpdates as $serviceName => $serviceUpdates) { - if (isset($current[$group][$serviceName]['instance_count']) === false) { - $current[$group][$serviceName]['instance_count'] = 1; - } - if (isset($current[$group][$serviceName]['disk']) === false) { - $current[$group][$serviceName]['disk'] = 0; - } - - $currentCount = $current[$group][$serviceName]['instance_count']; - $currentSize = $current[$group][$serviceName]['resources']['profile_size']; - $currentStorage = $current[$group][$serviceName]['disk']; + $currentService = $current[$group][$serviceName]; + $currentCount = $currentService['instance_count'] ?? 1; + $currentSize = $currentService['profile_size']; + $currentStorage = $currentService['disk'] ?? 0; $newCount = $currentCount; $newSize = $currentSize; @@ -883,8 +877,7 @@ private function computeMemoryCPUStorageDiff(array $updates, array $current): ar $newStorage = $serviceUpdates['disk']; } - $currentService = $current[$group][$serviceName]; - $currentProfile = $currentService['sizes'][$currentSize] ?? []; + $currentProfile = $currentSize !== null ? $currentService['sizes'][$currentSize] ?? [] : []; $currentCPU = $currentCount * ($currentProfile['cpu'] ?? 0); $currentRAM = $currentCount * ($currentProfile['memory'] ?? 0); diff --git a/legacy/src/Command/Resources/ResourcesSizeListCommand.php b/legacy/src/Command/Resources/ResourcesSizeListCommand.php index 4014e7b64..62b828208 100644 --- a/legacy/src/Command/Resources/ResourcesSizeListCommand.php +++ b/legacy/src/Command/Resources/ResourcesSizeListCommand.php @@ -64,10 +64,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $servicesByProfile = []; foreach ($services as $name => $service) { - $servicesByProfile[$service->container_profile][] = $name; + $profile = $this->resourcesUtil->containerProfile($service); + if ($profile !== null) { + $servicesByProfile[$profile][] = $name; + } } - $containerProfiles = $this->sortContainerProfiles($nextDeployment->container_profiles); + $containerProfiles = $this->sortContainerProfiles($this->resourcesUtil->containerProfiles($nextDeployment)); if ($serviceOption = Option::stringOrNull($input, 'service')) { if (!isset($services[$serviceOption])) { @@ -75,7 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } $service = $services[$serviceOption]; - $profile = $service->container_profile; + $profile = $this->resourcesUtil->containerProfile($service); } elseif ($profileOption = Option::stringOrNull($input, 'profile')) { $profile = $profileOption; if (!isset($containerProfiles[$profile])) { @@ -90,7 +93,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $profile = $this->questionHelper->choose($options, 'Enter a number to choose a container profile:'); } elseif (count($services) === 1) { $service = reset($services); - $profile = $service->container_profile; + $profile = $this->resourcesUtil->containerProfile($service); } else { throw new InvalidArgumentException('The --service or --profile is required.'); } @@ -101,7 +104,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($supportsGuaranteedCPU) { $defaultColumns[] = 'cpu_type'; } - foreach ($containerProfiles[$profile] as $sizeName => $sizeInfo) { + foreach ($containerProfiles[$profile] ?? [] as $sizeName => $sizeInfo) { if (!$supportsGuaranteedCPU && $sizeInfo['cpu_type'] === 'guaranteed') { continue; } @@ -109,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'size' => $sizeName, 'cpu' => $this->resourcesUtil->formatCPU($sizeInfo['cpu']), 'memory' => $sizeInfo['memory'], - 'cpu_type' => $sizeInfo['cpu_type'] ?? '', + 'cpu_type' => $sizeInfo['cpu_type'], ]; } diff --git a/legacy/src/Model/AutoscalingSettings.php b/legacy/src/Model/AutoscalingSettings.php new file mode 100644 index 000000000..d6adc2fae --- /dev/null +++ b/legacy/src/Model/AutoscalingSettings.php @@ -0,0 +1,119 @@ +, + * instances: array{min?: int, max?: int}, + * scale_cooldown: array{up?: int, down?: int}, + * } + */ +final class AutoscalingSettings +{ + /** + * @param ServiceSettings|null $defaults + * @param array $services + */ + private function __construct(public readonly ?array $defaults, public readonly array $services) {} + + /** + * @param array $data + */ + public static function fromData(array $data): self + { + $services = []; + if (isset($data['services']) && is_array($data['services'])) { + foreach ($data['services'] as $name => $settings) { + $services[(string) $name] = self::serviceSettings($settings); + } + } + $defaults = isset($data['defaults']) && is_array($data['defaults']) ? self::serviceSettings($data['defaults']) : null; + + return new self($defaults, $services); + } + + /** + * @return ServiceSettings + */ + private static function serviceSettings(mixed $data): array + { + $data = is_array($data) ? $data : []; + $settings = [ + 'triggers' => [], + 'instances' => [], + 'scale_cooldown' => [], + ]; + if (isset($data['enabled']) && is_bool($data['enabled'])) { + $settings['enabled'] = $data['enabled']; + } + if (isset($data['triggers']) && is_array($data['triggers'])) { + foreach ($data['triggers'] as $metric => $trigger) { + $settings['triggers'][(string) $metric] = self::trigger($trigger); + } + } + $instances = isset($data['instances']) && is_array($data['instances']) ? $data['instances'] : []; + if (isset($instances['min']) && is_int($instances['min'])) { + $settings['instances']['min'] = $instances['min']; + } + if (isset($instances['max']) && is_int($instances['max'])) { + $settings['instances']['max'] = $instances['max']; + } + $cooldown = isset($data['scale_cooldown']) && is_array($data['scale_cooldown']) ? $data['scale_cooldown'] : []; + if (isset($cooldown['up']) && is_int($cooldown['up'])) { + $settings['scale_cooldown']['up'] = $cooldown['up']; + } + if (isset($cooldown['down']) && is_int($cooldown['down'])) { + $settings['scale_cooldown']['down'] = $cooldown['down']; + } + + return $settings; + } + + /** + * @return Trigger + */ + private static function trigger(mixed $data): array + { + $data = is_array($data) ? $data : []; + $trigger = []; + if (isset($data['enabled']) && is_bool($data['enabled'])) { + $trigger['enabled'] = $data['enabled']; + } + if (isset($data['up']) && is_array($data['up'])) { + $trigger['up'] = self::condition($data['up']); + } + if (isset($data['down']) && is_array($data['down'])) { + $trigger['down'] = self::condition($data['down']); + } + + return $trigger; + } + + /** + * @param array $data + * + * @return Condition + */ + private static function condition(array $data): array + { + $condition = []; + if (isset($data['threshold']) && (is_int($data['threshold']) || is_float($data['threshold']))) { + $condition['threshold'] = (float) $data['threshold']; + } + if (isset($data['duration']) && is_int($data['duration'])) { + $condition['duration'] = $data['duration']; + } + + return $condition; + } +} diff --git a/legacy/src/Service/Api.php b/legacy/src/Service/Api.php index 27accbbc7..d2f1e98bb 100644 --- a/legacy/src/Service/Api.php +++ b/legacy/src/Service/Api.php @@ -31,6 +31,7 @@ use Platformsh\Cli\Exception\GoLoginRequiredException; use Platformsh\Cli\Exception\ProcessFailedException; use Platformsh\Cli\GuzzleDebugMiddleware; +use Platformsh\Cli\Model\AutoscalingSettings; use Platformsh\Cli\Model\Route; use Platformsh\Cli\Util\Jwt; use Platformsh\Cli\Util\NamespacedCache; @@ -39,7 +40,6 @@ use Platformsh\Cli\Connector; use Platformsh\Client\Exception\ApiResponseException; use Platformsh\Client\Exception\EnvironmentStateException; -use Platformsh\Client\Model\AutoscalingSettings; use Platformsh\Client\Model\BasicProjectInfo; use Platformsh\Client\Model\Deployment\EnvironmentDeployment; use Platformsh\Client\Model\Environment; @@ -2174,10 +2174,10 @@ public function getAutoscalingSettingsLink(Environment $environment, bool $manag * * @param Environment $environment * - * @return \Platformsh\Client\Model\AutoscalingSettings|false + * @return AutoscalingSettings|false * The autoscaling settings for the environment or false on failure. */ - public function getAutoscalingSettings(Environment $environment) + public function getAutoscalingSettings(Environment $environment): AutoscalingSettings|false { $autoscalingSettingsLink = $this->getAutoscalingSettingsLink($environment); if (!$autoscalingSettingsLink) { @@ -2192,7 +2192,7 @@ public function getAutoscalingSettings(Environment $environment) } return false; } - return new AutoscalingSettings($result->getData(), $autoscalingSettingsLink); + return AutoscalingSettings::fromData($result->getData()); } /** diff --git a/legacy/src/Service/Config.php b/legacy/src/Service/Config.php index 520c65b77..e9f060a24 100644 --- a/legacy/src/Service/Config.php +++ b/legacy/src/Service/Config.php @@ -63,14 +63,14 @@ public function __construct(?array $env = null, ?string $file = null) } catch (\InvalidArgumentException) { throw new \InvalidArgumentException('Invalid session ID in file: ' . $sessionIdFile); } - $this->config['api']['session_id'] = \trim($id); + $this->set('api.session_id', \trim($id)); } } } // Validate the session ID. - if (isset($this->config['api']['session_id'])) { - $this->validateSessionId($this->config['api']['session_id']); + if ($this->has('api.session_id')) { + $this->validateSessionId($this->getStr('api.session_id')); } } @@ -169,6 +169,50 @@ public function getBool(string $name): bool return (bool) $value; } + /** + * Get a string configuration value, or null if it is not defined. + * + * @throws \RuntimeException if the configuration is not a string. + */ + public function getStrOrNull(string $name): ?string + { + return $this->has($name) ? $this->getStr($name) : null; + } + + /** + * Get a list of strings from the configuration, which is empty if the list is not defined. + * + * A string is split into lines, as an environment variable can only set a string. + * + * @throws \RuntimeException if the configuration is not a list of strings. + * + * @return string[] + */ + public function getStrList(string $name): array + { + $value = $this->getWithDefault($name, []); + if (is_string($value)) { + return $value === '' ? [] : explode("\n", $value); + } + $list = []; + foreach (is_array($value) ? $value : [null] as $item) { + if (!is_string($item)) { + throw new \RuntimeException(sprintf('Configuration %s was expected to be a list of strings', $name)); + } + $list[] = $item; + } + + return $list; + } + + /** + * Sets a configuration value, replacing any existing one. + */ + private function set(string $name, mixed $value): void + { + NestedArrayUtil::setNestedArrayValue($this->config, explode('.', $name), $value, true); + } + /** * Get a configuration value, specifying a default if it does not exist. * @@ -202,7 +246,7 @@ public function getHomeDirectory(bool $reset = false): string if (!$reset && isset($this->homeDir)) { return $this->homeDir; } - $prefix = $this->config['application']['env_prefix'] ?? ''; + $prefix = $this->getStrOrNull('application.env_prefix') ?? ''; $envVars = [$prefix . 'HOME', 'HOME', 'USERPROFILE']; foreach ($envVars as $envVar) { $value = getenv($envVar); @@ -255,7 +299,7 @@ private function fs(): Filesystem */ public function getWritableUserDir(): string { - $path = $this->config['application']['writable_user_dir'] ?? $this->getUserConfigDir(false); + $path = $this->getStrOrNull('application.writable_user_dir') ?? $this->getUserConfigDir(false); $configDir = $this->getHomeDirectory() . DIRECTORY_SEPARATOR . $path; // If the directory is not writable (e.g. if we are on a Platform.sh @@ -282,7 +326,7 @@ public function getSessionDir(bool $subDir = false): string */ public function getSessionId(): string { - return $this->getWithDefault('api.session_id', 'default'); + return $this->getStrOrNull('api.session_id') ?? 'default'; } /** @@ -300,7 +344,7 @@ public function getSessionIdSlug(string $prefix = 'sess-cli-'): string */ public function setSessionId(string $id, bool $persist = false): void { - $this->config['api']['session_id'] = $id; + $this->set('api.session_id', $id); if ($persist) { $filename = $this->getSessionIdFile(); if ($id === 'default') { @@ -450,17 +494,17 @@ private function applyEnvironmentOverrides(): void // Special case: replace the list ssh.domain_wildcards with the value of {PREFIX}SSH_DOMAIN_WILDCARD. if (($value = $this->getEnv('SSH_DOMAIN_WILDCARD')) !== false) { - $this->config['ssh']['domain_wildcards'] = [$value]; + $this->set('ssh.domain_wildcards', [$value]); } // Special case: {PREFIX}NO_LEGACY_WARNING disables the migration prompt. if ($this->getEnv('NO_LEGACY_WARNING')) { - $this->config['migrate']['prompt'] = false; + $this->set('migrate.prompt', false); } // Special case: replace the list api.organization_types with the (split) value of {PREFIX}API_ORGANIZATION_TYPES. if (($value = $this->getEnv('API_ORGANIZATION_TYPES')) !== false) { - $this->config['api']['organization_types'] = $value === '' ? [] : \preg_split('/[,\s]+/', $value); + $this->set('api.organization_types', $value === '' ? [] : \preg_split('/[,\s]+/', $value)); } } @@ -477,7 +521,7 @@ private function applyEnvironmentOverrides(): void */ private function getEnv(string $name, bool $addPrefix = true): string|false { - $prefix = $addPrefix && isset($this->config['application']['env_prefix']) ? $this->config['application']['env_prefix'] : ''; + $prefix = $addPrefix ? $this->getStrOrNull('application.env_prefix') ?? '' : ''; if (array_key_exists($prefix . $name, $this->env)) { return $this->env[$prefix . $name]; } @@ -502,7 +546,7 @@ private function applyUserConfigOverrides(): void */ public function isExperimentEnabled(string $name): bool { - return !empty($this->config['experimental']['all_experiments']) || !empty($this->config['experimental'][$name]); + return !empty($this->getWithDefault('experimental.all_experiments', false)) || !empty($this->getWithDefault('experimental.' . $name, false)); } /** @@ -510,8 +554,7 @@ public function isExperimentEnabled(string $name): bool */ public function isCommandHidden(string $name): bool { - return (!empty($this->config['application']['hidden_commands']) - && in_array($name, $this->config['application']['hidden_commands'])); + return in_array($name, $this->getStrList('application.hidden_commands')); } /** @@ -519,22 +562,15 @@ public function isCommandHidden(string $name): bool */ public function isCommandEnabled(string $name): bool { - if (!empty($this->config['application']['disabled_commands']) - && in_array($name, $this->config['application']['disabled_commands'])) { + if (in_array($name, $this->getStrList('application.disabled_commands'))) { return false; } - if ($this->isWrapped() - && !empty($this->config['application']['wrapped_disabled_commands']) - && in_array($name, $this->config['application']['wrapped_disabled_commands'])) { + if ($this->isWrapped() && in_array($name, $this->getStrList('application.wrapped_disabled_commands'))) { return false; } - if (!empty($this->config['application']['experimental_commands']) - && in_array($name, $this->config['application']['experimental_commands'])) { - return !empty($this->config['experimental']['all_experiments']) - || ( - !empty($this->config['experimental']['enable_commands']) - && in_array($name, $this->config['experimental']['enable_commands']) - ); + if (in_array($name, $this->getStrList('application.experimental_commands'))) { + return !empty($this->getWithDefault('experimental.all_experiments', false)) + || in_array($name, $this->getStrList('experimental.enable_commands')); } return true; @@ -548,8 +584,8 @@ public function getVersion(): string if (isset($this->version)) { return $this->version; } - $version = $this->getWithDefault('application.version', '@version-placeholder@'); - if (str_starts_with((string) $version, '@') && str_ends_with((string) $version, '@') && !str_starts_with(CLI_ROOT, 'phar://')) { + $version = $this->getStrOrNull('application.version') ?? '@version-placeholder@'; + if (str_starts_with($version, '@') && str_ends_with($version, '@') && !str_starts_with(CLI_ROOT, 'phar://')) { // Silently try getting the version from Git. $tag = (new Shell())->execute(['git', 'describe', '--tags'], CLI_ROOT); if (is_string($tag) && str_starts_with($tag, 'v')) { @@ -568,10 +604,7 @@ public function getVersion(): string */ public function getUserAgent(): string { - $template = $this->getWithDefault( - 'api.user_agent', - '{APP_NAME_DASH}/{VERSION} ({UNAME_S}; {UNAME_R}; PHP {PHP_VERSION})', - ); + $template = $this->getStrOrNull('api.user_agent') ?? '{APP_NAME_DASH}/{VERSION} ({UNAME_S}; {UNAME_R}; PHP {PHP_VERSION})'; /** @var array $replacements */ $replacements = [ '{APP_NAME_DASH}' => \str_replace(' ', '-', $this->getStr('application.name')), @@ -593,12 +626,12 @@ public function getUserAgent(): string public function getProxies(): array { $proxies = []; - if (\getenv('https_proxy') !== false) { - $proxies['https'] = \getenv('https_proxy'); + if (($https = \getenv('https_proxy')) !== false) { + $proxies['https'] = $https; } // An environment variable prefixed by 'http_' cannot be trusted in a non-CLI (web) context. - if (PHP_SAPI === 'cli' && \getenv('http_proxy') !== false) { - $proxies['http'] = \getenv('http_proxy'); + if (PHP_SAPI === 'cli' && ($http = \getenv('http_proxy')) !== false) { + $proxies['http'] = $http; } return $proxies; } @@ -730,43 +763,44 @@ private function applyDynamicDefaults(): void $this->applyUrlDefaults(); $this->applyLocalDirectoryDefaults(); - if (!isset($this->config['application']['slug'])) { - $this->config['application']['slug'] = preg_replace('/[^a-z0-9-]+/', '-', str_replace(['.', ' '], ['', '-'], strtolower($this->getStr('application.name')))); + if (!$this->has('application.slug')) { + $this->set('application.slug', preg_replace('/[^a-z0-9-]+/', '-', str_replace(['.', ' '], ['', '-'], strtolower($this->getStr('application.name'))))); } - if (!isset($this->config['application']['tmp_sub_dir'])) { - $this->config['application']['tmp_sub_dir'] = $this->getStr('application.slug') . '-tmp'; + if (!$this->has('application.tmp_sub_dir')) { + $this->set('application.tmp_sub_dir', $this->getStr('application.slug') . '-tmp'); } - if (!isset($this->config['api']['oauth2_client_id'])) { - $this->config['api']['oauth2_client_id'] = $this->getStr('application.slug'); + if (!$this->has('api.oauth2_client_id')) { + $this->set('api.oauth2_client_id', $this->getStr('application.slug')); } - if (!isset($this->config['detection']['console_domain']) && isset($this->config['service']['console_url'])) { - $consoleDomain = parse_url((string) $this->config['service']['console_url'], PHP_URL_HOST); + if (!$this->has('detection.console_domain') && $this->has('service.console_url')) { + $consoleDomain = parse_url($this->getStr('service.console_url'), PHP_URL_HOST); if ($consoleDomain !== false) { - $this->config['detection']['console_domain'] = $consoleDomain; + $this->set('detection.console_domain', $consoleDomain); } } - if (!isset($this->config['service']['applications_config_file'])) { - $this->config['service']['applications_config_file'] = $this->getStr('service.project_config_dir') . '/applications.yaml'; + if (!$this->has('service.applications_config_file')) { + $this->set('service.applications_config_file', $this->getStr('service.project_config_dir') . '/applications.yaml'); } // Migrate renamed config keys. - if (isset($this->config['api']['add_to_ssh_agent']) && !isset($this->config['ssh']['add_to_agent'])) { - $this->config['ssh']['add_to_agent'] = $this->config['api']['add_to_ssh_agent']; - } - if (isset($this->config['api']['auto_load_ssh_cert']) && !isset($this->config['ssh']['auto_load_cert'])) { - $this->config['ssh']['auto_load_cert'] = $this->config['api']['auto_load_ssh_cert']; - } - if (isset($this->config['api']['ssh_domain_wildcards']) && !isset($this->config['ssh']['domain_wildcards'])) { - $this->config['ssh']['domain_wildcards'] = $this->config['api']['ssh_domain_wildcards']; + $renamed = [ + 'api.add_to_ssh_agent' => 'ssh.add_to_agent', + 'api.auto_load_ssh_cert' => 'ssh.auto_load_cert', + 'api.ssh_domain_wildcards' => 'ssh.domain_wildcards', + ]; + foreach ($renamed as $old => $new) { + if ($this->has($old) && !$this->has($new)) { + $this->set($new, $this->get($old)); + } } - if (isset($this->config['service']['header_prefix']) && !isset($this->config['detection']['cluster_header'])) { - $this->config['detection']['cluster_header'] = $this->config['service']['header_prefix'] . '-Cluster'; + if ($this->has('service.header_prefix') && !$this->has('detection.cluster_header')) { + $this->set('detection.cluster_header', $this->getStr('service.header_prefix') . '-Cluster'); } } private function applyUrlDefaults(): void { - $authUrl = $this->getWithDefault('api.auth_url', ''); + $authUrl = $this->getStrOrNull('api.auth_url') ?? ''; if ($authUrl === '') { return; } @@ -777,19 +811,18 @@ private function applyUrlDefaults(): void 'certifier_url' => '', ]; foreach ($defaultsUnderAuthUrl as $apiSubKey => $path) { - if (!isset($this->config['api'][$apiSubKey])) { - $this->config['api'][$apiSubKey] = rtrim((string) $authUrl, '/') . $path; + if (!$this->has('api.' . $apiSubKey)) { + $this->set('api.' . $apiSubKey, rtrim($authUrl, '/') . $path); } } } private function applyLocalDirectoryDefaults(): void { - if (isset($this->config['local']['local_dir'])) { - $localDir = $this->config['local']['local_dir']; - } else { + $localDir = $this->getStrOrNull('local.local_dir'); + if ($localDir === null) { $localDir = $this->getStr('service.project_config_dir') . DIRECTORY_SEPARATOR . 'local'; - $this->config['local']['local_dir'] = $localDir; + $this->set('local.local_dir', $localDir); } $defaultsUnderLocalDir = [ 'archive_dir' => 'build-archives', @@ -799,8 +832,8 @@ private function applyLocalDirectoryDefaults(): void 'shared_dir' => 'shared', ]; foreach ($defaultsUnderLocalDir as $localSubKey => $subPath) { - if (!isset($this->config['local'][$localSubKey])) { - $this->config['local'][$localSubKey] = $localDir . DIRECTORY_SEPARATOR . $subPath; + if (!$this->has('local.' . $localSubKey)) { + $this->set('local.' . $localSubKey, $localDir . DIRECTORY_SEPARATOR . $subPath); } } } diff --git a/legacy/src/Service/Identifier.php b/legacy/src/Service/Identifier.php index 355c2ce4d..6339e063e 100644 --- a/legacy/src/Service/Identifier.php +++ b/legacy/src/Service/Identifier.php @@ -79,7 +79,7 @@ private function parseProjectId(string $url): array $path = $urlParts['path'] ?? ''; $fragment = $urlParts['fragment'] ?? ''; - $site_domains_pattern = '(' . implode('|', array_map('preg_quote', (array) $this->config->get('detection.site_domains'))) . ')'; + $site_domains_pattern = '(' . implode('|', array_map(fn(string $domain): string => preg_quote($domain), $this->config->getStrList('detection.site_domains'))) . ')'; $site_pattern = '/\-\w+\.[a-z]{2}(\-[0-9])?\.' . $site_domains_pattern . '$/'; if (preg_match($site_pattern, $host)) { diff --git a/legacy/src/Service/LegacyMigration.php b/legacy/src/Service/LegacyMigration.php index ad2f705a2..b40126f72 100644 --- a/legacy/src/Service/LegacyMigration.php +++ b/legacy/src/Service/LegacyMigration.php @@ -163,7 +163,7 @@ private function checkMigrateToNewCLI(): void // Prompt the user to migrate at most once every 24 hours. $now = time(); - $embargoTime = $now - $this->config->getWithDefault('migrate.prompt_interval', 60 * 60 * 24); + $embargoTime = $now - ($this->config->has('migrate.prompt_interval') ? $this->config->getInt('migrate.prompt_interval') : 60 * 60 * 24); if ($this->state->get('migrate.last_prompted') > $embargoTime) { return; } diff --git a/legacy/src/Service/PropertyFormatter.php b/legacy/src/Service/PropertyFormatter.php index af0b53672..1b96bf75e 100644 --- a/legacy/src/Service/PropertyFormatter.php +++ b/legacy/src/Service/PropertyFormatter.php @@ -130,8 +130,7 @@ public function formatDate(int|string $value): string $dateTime = new \DateTime($value); } $dateTime->setTimezone(new \DateTimeZone( - $this->config->getWithDefault('application.timezone', null) - ?: TimezoneUtil::getTimezone(), + $this->config->getStrOrNull('application.timezone') ?: TimezoneUtil::getTimezone(), )); return $dateTime->format($this->dateFormat()); diff --git a/legacy/src/Service/QuestionHelper.php b/legacy/src/Service/QuestionHelper.php index 05f2e8b77..a06c9d586 100644 --- a/legacy/src/Service/QuestionHelper.php +++ b/legacy/src/Service/QuestionHelper.php @@ -188,13 +188,15 @@ public function chooseAssoc(array $items, string $text = 'Choose an item:', ?str /** * Ask a simple question which requires input. * + * @template T + * * @param string $questionText * @param mixed|null $default * @param string[] $autoCompleterValues - * @param callable|null $validator + * @param (callable(string): T)|null $validator * @param string $defaultLabel - * @return mixed - * The user's answer. + * @return ($validator is null ? mixed : T) + * The user's answer, as returned by the validator if there is one. */ public function askInput(string $questionText, mixed $default = null, array $autoCompleterValues = [], ?callable $validator = null, string $defaultLabel = 'default: '): mixed { @@ -213,7 +215,7 @@ public function askInput(string $questionText, mixed $default = null, array $aut $question->setAutocompleterValues($autoCompleterValues); } if ($validator !== null) { - $question->setValidator($validator); + $question->setValidator($validator); // @phpstan-ignore argument.type (Symfony passes the answer, a string unless there is no default.) $question->setMaxAttempts(5); } diff --git a/legacy/src/Service/ResourcesUtil.php b/legacy/src/Service/ResourcesUtil.php index dfb6273b3..3eaa7ee4c 100644 --- a/legacy/src/Service/ResourcesUtil.php +++ b/legacy/src/Service/ResourcesUtil.php @@ -17,12 +17,26 @@ use Platformsh\Client\Model\Deployment\Worker; use Platformsh\Client\Model\Environment; use Platformsh\Client\Model\Project; +use Platformsh\Client\Model\Project\Settings; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * @phpstan-type Sizing array{cpu?: float, memory?: int, disk?: int, profile_size?: string} + * @phpstan-type ContainerResources array{ + * profile_size?: string, + * base_memory?: int, + * memory_ratio?: int, + * minimum: Sizing, + * default: Sizing, + * object_storage?: int, + * } + * @phpstan-type ProfileSize array{cpu: float, memory: int, cpu_type: string} + * @phpstan-type ContainerProfiles array> + */ class ResourcesUtil { private readonly OutputInterface $stdErr; @@ -57,7 +71,7 @@ public function writeSizingApiDisabledError(Project $project): void private function isFixedProject(Project $project): bool { $orgId = $project->getProperty('organization', false, false); - if (!$orgId || !$this->config->getBool('api.organizations')) { + if (!is_string($orgId) || $orgId === '' || !$this->config->getBool('api.organizations')) { return false; } try { @@ -87,7 +101,13 @@ public function allServices(EnvironmentDeployment $deployment): array ksort($workers, SORT_STRING | SORT_FLAG_CASE); ksort($services, SORT_STRING | SORT_FLAG_CASE); ksort($tasks, SORT_STRING | SORT_FLAG_CASE); - return array_merge($webapps, $workers, $services, $tasks); + $all = []; + foreach ([$webapps, $workers, $services, $tasks] as $containers) { + foreach ($containers as $name => $container) { + $all[(string) $name] = $container; + } + } + return $all; } /** @@ -102,7 +122,7 @@ public function supportsDisk(WebApp|Worker|Service|Task $service): bool if ($service instanceof Worker || $service instanceof Task) { return false; } - return isset($service->getProperties()['resources']['minimum']['disk']); + return isset($this->resources($service)['minimum']['disk']); } /** @@ -135,7 +155,7 @@ public function loadNextDeployment(Environment $environment, bool $reset = false * @param array $services * @param InputInterface $input * - * @return WebApp[]|Service[]|Worker[]|Task[]|false + * @return array|false * False on error, or an array of services. */ public function filterServices(array $services, InputInterface $input): array|false @@ -208,26 +228,127 @@ public function filterServices(array $services, InputInterface $input): array|fa } /** - * Returns container profile size info, given service properties. + * Returns a container's resources, typed from its deployment properties. + * + * Values that are missing or of an unexpected type are left out. + * + * @return ContainerResources + */ + public function resources(WebApp|Worker|Service|Task $service): array + { + $data = $service->getProperties()['resources'] ?? null; + $data = is_array($data) ? $data : []; + $resources = [ + 'minimum' => self::sizing($data['minimum'] ?? null), + 'default' => self::sizing($data['default'] ?? null), + ]; + if (isset($data['profile_size']) && (is_string($data['profile_size']) || is_int($data['profile_size']) || is_float($data['profile_size']))) { + $resources['profile_size'] = (string) $data['profile_size']; + } + if (isset($data['base_memory']) && is_int($data['base_memory'])) { + $resources['base_memory'] = $data['base_memory']; + } + if (isset($data['memory_ratio']) && is_int($data['memory_ratio'])) { + $resources['memory_ratio'] = $data['memory_ratio']; + } + $disk = isset($data['disk']) && is_array($data['disk']) ? $data['disk'] : []; + if (isset($disk['object']) && is_int($disk['object'])) { + $resources['object_storage'] = $disk['object']; + } + + return $resources; + } + + /** + * @return Sizing + */ + private static function sizing(mixed $data): array + { + $data = is_array($data) ? $data : []; + $sizing = []; + if (isset($data['cpu']) && (is_int($data['cpu']) || is_float($data['cpu']))) { + $sizing['cpu'] = (float) $data['cpu']; + } + if (isset($data['memory']) && is_int($data['memory'])) { + $sizing['memory'] = $data['memory']; + } + if (isset($data['disk']) && is_int($data['disk'])) { + $sizing['disk'] = $data['disk']; + } + if (isset($data['profile_size']) && (is_string($data['profile_size']) || is_int($data['profile_size']) || is_float($data['profile_size']))) { + $sizing['profile_size'] = (string) $data['profile_size']; + } + + return $sizing; + } + + /** + * Returns the container profiles of a deployment, typed. * - * @param array $properties - * The service properties (e.g. from $service->getProperties()). - * @param array $containerProfiles - * The list of container profiles (e.g. from - * $deployment->container_profiles). + * Sizes without a CPU, memory and CPU type are left out. * - * @return array{'cpu': string, 'memory': string}|null + * @return ContainerProfiles */ - public function sizeInfo(array $properties, array $containerProfiles): ?array + public function containerProfiles(EnvironmentDeployment $deployment): array { - if (isset($properties['resources']['profile_size'])) { - $size = $properties['resources']['profile_size']; - $profile = $properties['container_profile']; - if (isset($containerProfiles[$profile][$size])) { - return $containerProfiles[$profile][$size]; + $data = $deployment->getProperty('container_profiles', false); + $profiles = []; + foreach (is_array($data) ? $data : [] as $profileName => $sizes) { + $profiles[(string) $profileName] = []; + foreach (is_array($sizes) ? $sizes : [] as $sizeName => $size) { + if (is_array($size) && isset($size['cpu'], $size['memory'], $size['cpu_type']) + && (is_int($size['cpu']) || is_float($size['cpu'])) + && is_int($size['memory']) + && is_string($size['cpu_type'])) { + $profiles[(string) $profileName][$sizeName] = ['cpu' => (float) $size['cpu'], 'memory' => $size['memory'], 'cpu_type' => $size['cpu_type']]; + } } } - return null; + + return $profiles; + } + + /** + * Returns a project's build resources, with null for a value that is missing or of an unexpected type. + * + * @return array{cpu: float|null, memory: int|null} + */ + public function buildResources(Settings $settings): array + { + $current = $settings['build_resources'] ?? null; + $cpu = is_array($current) ? $current['cpu'] ?? null : null; + $memory = is_array($current) ? $current['memory'] ?? null : null; + + return [ + 'cpu' => is_int($cpu) || is_float($cpu) ? (float) $cpu : null, + 'memory' => is_int($memory) ? $memory : null, + ]; + } + + /** + * Returns a container's profile name: tasks may lack one, and default to BALANCED. + */ + public function containerProfile(WebApp|Worker|Service|Task $service): ?string + { + return $service->container_profile ?? ($service instanceof Task ? 'BALANCED' : null); + } + + /** + * Returns a container's instance count, if it is set. + */ + public function instanceCount(WebApp|Worker|Service|Task $service): ?int + { + $count = $service->getProperties()['instance_count'] ?? null; + return is_int($count) ? $count : null; + } + + /** + * Returns a container's disk size in MB, if it is set. + */ + public function disk(WebApp|Worker|Service|Task $service): ?int + { + $disk = $service->getProperties()['disk'] ?? null; + return is_int($disk) ? $disk : null; } /** diff --git a/legacy/src/Service/Ssh.php b/legacy/src/Service/Ssh.php index a88020170..3e196347d 100644 --- a/legacy/src/Service/Ssh.php +++ b/legacy/src/Service/Ssh.php @@ -158,9 +158,7 @@ private function getSshOptions(?bool $hostIsInternal): array } } - if ($configuredOptions = $this->config->get('ssh.options')) { - $options = array_merge($options, is_array($configuredOptions) ? $configuredOptions : explode("\n", (string) $configuredOptions)); - } + $options = array_merge($options, $this->config->getStrList('ssh.options')); // Avoid repeating options. return array_unique($options); diff --git a/legacy/src/Service/SshConfig.php b/legacy/src/Service/SshConfig.php index 3e131a422..f3a1abf6a 100644 --- a/legacy/src/Service/SshConfig.php +++ b/legacy/src/Service/SshConfig.php @@ -40,10 +40,7 @@ public function configureHostKeys(): ?string return null; } } - if ($additionalKeys = $this->config->getWithDefault('ssh.host_keys', [])) { - if (!is_array($additionalKeys)) { - $additionalKeys = explode("\n", (string) $additionalKeys); - } + if ($additionalKeys = $this->config->getStrList('ssh.host_keys')) { $hostKeys = rtrim($hostKeys, "\n") . "\n" . implode("\n", $additionalKeys); } if (empty($hostKeys)) { @@ -147,11 +144,10 @@ public function configureSessionSsh(): bool $includerFilename = $this->getCliSshDir() . DIRECTORY_SEPARATOR . 'session.config'; // Add other configured options. - if ($configuredOptions = $this->config->get('ssh.options')) { - /** @var string[]|string $configuredOptions */ + if ($configuredOptions = $this->config->getStrList('ssh.options')) { $lines[] = ''; $lines[] = "# Other options from the CLI's configuration."; - $lines = array_merge($lines, is_array($configuredOptions) ? $configuredOptions : explode("\n", $configuredOptions)); + $lines = array_merge($lines, $configuredOptions); } $this->writeSshIncludeFile($sessionSpecificFilename, $lines); diff --git a/legacy/src/Service/State.php b/legacy/src/Service/State.php index 26f5a5339..e11f333fd 100644 --- a/legacy/src/Service/State.php +++ b/legacy/src/Service/State.php @@ -80,6 +80,6 @@ protected function load(): void */ protected function getFilename(): string { - return $this->config->getWritableUserDir() . DIRECTORY_SEPARATOR . $this->config->getWithDefault('application.user_state_file', 'state.json'); + return $this->config->getWritableUserDir() . DIRECTORY_SEPARATOR . ($this->config->getStrOrNull('application.user_state_file') ?? 'state.json'); } } diff --git a/legacy/src/Service/TokenConfig.php b/legacy/src/Service/TokenConfig.php index b98bb2280..a7b644975 100644 --- a/legacy/src/Service/TokenConfig.php +++ b/legacy/src/Service/TokenConfig.php @@ -28,12 +28,12 @@ public function storage(): StorageInterface */ public function getApiToken(bool $includeStored = true): ?string { - $token = (string) $this->config->getWithDefault('api.token', ''); + $token = $this->config->getStrOrNull('api.token') ?? ''; if ($token !== '') { return $token; } - $file = (string) $this->config->getWithDefault('api.token_file', ''); + $file = $this->config->getStrOrNull('api.token_file') ?? ''; if ($file !== '') { $token = $this->loadTokenFromFile($file); if ($token !== '') { @@ -53,7 +53,7 @@ public function getApiToken(bool $includeStored = true): ?string public function getAccessToken(): ?string { - return $this->config->getWithDefault('api.access_token', null); + return $this->config->getStrOrNull('api.access_token'); } /** diff --git a/legacy/tests/Command/Resources/ResourcesSetTest.php b/legacy/tests/Command/Resources/ResourcesSetTest.php index 3b6b8847f..07a40920a 100644 --- a/legacy/tests/Command/Resources/ResourcesSetTest.php +++ b/legacy/tests/Command/Resources/ResourcesSetTest.php @@ -84,7 +84,7 @@ public function testDiskIsRequiredButUnset(): void if ($disk !== null) { $properties['disk'] = $disk; } - $this->assertSame($expected, $m->invoke($command, $properties), sprintf( + $this->assertSame($expected, $m->invoke($command, WebApp::fromData($properties)), sprintf( 'minimum %s, disk %s', var_export($minimum, true), var_export($disk, true), diff --git a/legacy/tests/ConfigTest.php b/legacy/tests/ConfigTest.php index 650472110..1be74f920 100644 --- a/legacy/tests/ConfigTest.php +++ b/legacy/tests/ConfigTest.php @@ -57,6 +57,32 @@ public function testEnvironmentOverrides(): void $this->assertEquals('Overridden application name', $config->get('application.name')); } + public function testGetStrList(): void + { + $config = (new Config([], $this->configFile))->withOverrides([ + 'test.list' => ['a', 'b'], + 'test.lines' => "a\nb", + 'test.empty' => '', + 'test.null' => null, + 'test.mixed' => ['a', 1], + ]); + $this->assertSame(['a', 'b'], $config->getStrList('test.list')); + $this->assertSame(['a', 'b'], $config->getStrList('test.lines')); + $this->assertSame([], $config->getStrList('test.empty')); + $this->assertSame([], $config->getStrList('test.null')); + $this->assertSame([], $config->getStrList('test.undefined')); + $this->expectException(\RuntimeException::class); + $config->getStrList('test.mixed'); + } + + public function testGetStrOrNull(): void + { + $config = (new Config([], $this->configFile))->withOverrides(['test.str' => 'a', 'test.null' => null]); + $this->assertSame('a', $config->getStrOrNull('test.str')); + $this->assertNull($config->getStrOrNull('test.null')); + $this->assertNull($config->getStrOrNull('test.undefined')); + } + /** * Test that selected user config can override initial config. */