Skip to content

Commit d443d0a

Browse files
committed
Add PHPStan CI workflow and fix all level 9 errors.
1 parent 90373e6 commit d443d0a

11 files changed

Lines changed: 185 additions & 49 deletions

File tree

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
phpstan:
10+
name: PHPStan
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v6
15+
16+
- name: Setup PHP
17+
uses: shivammathur/setup-php@v2
18+
with:
19+
php-version: '8.3'
20+
tools: composer:v2
21+
coverage: none
22+
23+
- name: Cache Composer dependencies
24+
uses: actions/cache@v5
25+
with:
26+
path: ~/.composer/cache
27+
key: php-composer-locked-${{ hashFiles('composer.lock') }}
28+
restore-keys: php-composer-locked-
29+
30+
- name: Install dependencies
31+
run: composer install --no-interaction --prefer-dist --no-progress
32+
33+
- name: PHPStan
34+
run: vendor/bin/phpstan analyse --no-progress

src/Application.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ public function __construct()
3737

3838
public function safeAddCommand(Command $command): ?Command
3939
{
40+
// addCommand() exists since Symfony Console 7.4; add() was removed in later versions.
41+
/** @phpstan-ignore function.alreadyNarrowedType */
4042
if (method_exists($this, 'addCommand')) {
41-
// addCommand() exists since 7.4 and add() has been deprecated.
4243
return $this->addCommand($command);
4344
}
4445

46+
/** @phpstan-ignore method.notFound */
4547
return $this->add($command);
4648
}
4749

src/BrefCloudClient.php

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ public static function getUrl(): string
7171
*/
7272
public function getUserInfo(): array
7373
{
74-
return $this->client->request('GET', '/api/v1/user')->toArray();
74+
/** @var array{id: int, name: string, email: string} $result */
75+
$result = $this->client->request('GET', '/api/v1/user')->toArray();
76+
77+
return $result;
7578
}
7679

7780
/**
@@ -102,9 +105,12 @@ public function createDeployment(string $environment, array $config, string|null
102105
if ($awsAccountName) {
103106
$body['aws_account_name'] = $awsAccountName;
104107
}
105-
return $this->client->request('POST', '/api/v1/deployments', [
108+
/** @var array{deploymentId: int, message: string, url: string, credentials?: array{accessKeyId: string, secretAccessKey: string, sessionToken: string}, packageUrls?: array<string, string>} $result */
109+
$result = $this->client->request('POST', '/api/v1/deployments', [
106110
'json' => $body,
107111
])->toArray();
112+
113+
return $result;
108114
}
109115

110116
public function startDeployment(int $deploymentId): void
@@ -129,7 +135,10 @@ public function startDeployment(int $deploymentId): void
129135
*/
130136
public function getDeployment(int $deploymentId): array
131137
{
132-
return $this->client->request('GET', "/api/v1/deployments/$deploymentId")->toArray();
138+
/** @var array{deploymentId: int, status: string, message: string, error_message: string|null, url: string, app_url: string|null, logs: list<array{line: string, timestamp: int}>, outputs?: array<string, string>} $result */
139+
$result = $this->client->request('GET', "/api/v1/deployments/$deploymentId")->toArray();
140+
141+
return $result;
133142
}
134143

135144
public function pushDeploymentLogs(int $deploymentId, string $newLogs): void
@@ -189,7 +198,10 @@ public function markDeploymentFinished(
189198
*/
190199
public function getEnvironment(int $id): array
191200
{
192-
return $this->client->request('GET', '/api/v1/environments/' . $id)->toArray();
201+
/** @var array{id: int, name: string, region: string|null, url: string|null, outputs: array<string, string>, app: array{id: int, name: string}, aws_account_id: int|null} $result */
202+
$result = $this->client->request('GET', '/api/v1/environments/' . $id)->toArray();
203+
204+
return $result;
193205
}
194206

195207
/**
@@ -208,11 +220,14 @@ public function getEnvironment(int $id): array
208220
*/
209221
public function findEnvironment(string $teamSlug, string $appName, string $environment): array
210222
{
211-
return $this->client->request('GET', '/api/v1/environments/find?' . http_build_query([
223+
/** @var array{id: int, name: string, region: string|null, url: string|null, outputs: array<string, string>, app: array{id: int, name: string}, aws_account_id: int|null} $result */
224+
$result = $this->client->request('GET', '/api/v1/environments/find?' . http_build_query([
212225
'teamSlug' => $teamSlug,
213226
'appName' => $appName,
214227
'environmentName' => $environment,
215228
]))->toArray();
229+
230+
return $result;
216231
}
217232

218233
public function removeEnvironment(int $environmentId): void
@@ -245,7 +260,10 @@ public function startCommand(int $environmentId, string $command): int
245260
*/
246261
public function getCommand(int $id): array
247262
{
248-
return $this->client->request('GET', "/api/v1/commands/$id")->toArray();
263+
/** @var array{status: 'not_started'|'started'|'success'|'failed', output: string} $result */
264+
$result = $this->client->request('GET', "/api/v1/commands/$id")->toArray();
265+
266+
return $result;
249267
}
250268

251269
/**
@@ -256,7 +274,10 @@ public function getCommand(int $id): array
256274
*/
257275
public function listAwsAccounts(): array
258276
{
259-
return $this->client->request('GET', '/api/v1/aws-accounts')->toArray();
277+
/** @var list<array{id: int, name: string, role_arn: string, team_id: int}> $result */
278+
$result = $this->client->request('GET', '/api/v1/aws-accounts')->toArray();
279+
280+
return $result;
260281
}
261282

262283
/**
@@ -267,7 +288,10 @@ public function listAwsAccounts(): array
267288
*/
268289
public function listTeams(): array
269290
{
270-
return $this->client->request('GET', '/api/v1/teams')->toArray();
291+
/** @var list<array{id: int, name: string, slug: string}> $result */
292+
$result = $this->client->request('GET', '/api/v1/teams')->toArray();
293+
294+
return $result;
271295
}
272296

273297
/**
@@ -285,7 +309,10 @@ public function listTeams(): array
285309
*/
286310
public function prepareConnectAwsAccount(int $teamId): array
287311
{
288-
return $this->client->request('GET', '/api/v1/aws-accounts/connect?team_id=' . $teamId)->toArray();
312+
/** @var array{region: string, template_url: string, stack_name: string, bref_cloud_account_id: string, unique_external_id: string, role_name: string|null} $result */
313+
$result = $this->client->request('GET', '/api/v1/aws-accounts/connect?team_id=' . $teamId)->toArray();
314+
315+
return $result;
289316
}
290317

291318
public function addAwsAccount(mixed $teamId, string $accountName, string $roleArn): void

src/Commands/ApplicationCommand.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ protected function parseStandardOptions(InputInterface $input): array
3232
$environment = $input->getOption('env');
3333
/** @var string|null $configFileName */
3434
$configFileName = $input->getOption('config');
35-
$config = Config::loadConfig($configFileName, $environment, $input->getOption('team'));
35+
/** @var string|null $overrideTeam */
36+
$overrideTeam = $input->getOption('team');
37+
$config = Config::loadConfig($configFileName, $environment, $overrideTeam);
3638

3739
return [
3840
'appName' => $config['name'],

src/Commands/Command.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ private function writeErrorDetails(string $output): void
9696
$errorDetails['errorMessage'],
9797
]);
9898
if (isset($errorDetails['stackTrace']) && is_array($errorDetails['stackTrace'])) {
99-
IO::verbose($errorDetails['stackTrace']);
99+
$stackTrace = array_values(array_filter($errorDetails['stackTrace'], 'is_string'));
100+
IO::verbose($stackTrace);
100101
}
101102
} catch (JsonException) {
102103
IO::writeln(Styles::red($output));

src/Commands/Connect.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8484
]);
8585

8686
$question = new Question('Display name: ');
87-
$question->setValidator(function (string $answer) use ($existingAccounts): string {
87+
$question->setValidator(function (mixed $answer) use ($existingAccounts): string {
88+
if (! is_string($answer) || $answer === '') {
89+
throw new Exception('Account name cannot be empty');
90+
}
8891
// Check if the name is already taken
8992
foreach ($existingAccounts as $account) {
9093
if ($account['name'] === $answer) {

src/Commands/Deploy.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,12 @@ private function uploadArtifacts(array $config, array $packageUrls): void
271271

272272
$archivePaths = [];
273273
foreach ($packageUrls as $id => $url) {
274-
$package = $config['packages'][$id];
275-
$archivePaths[$id] = $this->packageArtifact($id, $package['path'], $package['patterns']);
274+
$package = $config['packages'][$id] ?? null;
275+
if (! is_array($package) || ! isset($package['path'], $package['patterns']) || ! is_string($package['path']) || ! is_array($package['patterns'])) {
276+
throw new Exception("Invalid package configuration for '$id'");
277+
}
278+
$patterns = array_values(array_filter($package['patterns'], 'is_string'));
279+
$archivePaths[$id] = $this->packageArtifact($id, $package['path'], $patterns);
276280
}
277281

278282
IO::spin('uploading');

src/Commands/SecretCreate.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
3131
{
3232
// If --app and --team are provided, we can skip loading the config file
3333
if ($input->getOption('app') && $input->getOption('team')) {
34+
/** @var string $appName */
3435
$appName = $input->getOption('app');
36+
/** @var string $team */
3537
$team = $input->getOption('team');
38+
/** @var string $environment */
3639
$environment = $input->getOption('env');
3740
} else {
3841
[
@@ -43,22 +46,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int
4346

4447
// Override app name if --app option provided
4548
if ($input->getOption('app')) {
46-
$appName = $input->getOption('app');
49+
$appOption = $input->getOption('app');
50+
if (! is_string($appOption)) {
51+
throw new Exception('Invalid app name');
52+
}
53+
$appName = $appOption;
4754
}
4855
}
4956

5057
// Get secret name (from argument or prompt)
5158
$name = $input->getArgument('name');
5259
if (empty($name)) {
5360
$question = new Question('Secret name: ');
54-
$question->setValidator(function (?string $value): string {
55-
if (empty($value)) {
61+
$question->setValidator(function (mixed $value): string {
62+
if (! is_string($value) || $value === '') {
5663
throw new Exception('Secret name cannot be empty');
5764
}
5865
return $value;
5966
});
6067
$name = IO::ask($question);
6168
}
69+
if (! is_string($name)) {
70+
throw new Exception('Secret name cannot be empty');
71+
}
6272

6373
// Get secret value (from argument or prompt)
6474
$value = $input->getArgument('value');
@@ -67,6 +77,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6777
$question->setHidden(true)->setHiddenFallback(false);
6878
$value = IO::ask($question);
6979
}
80+
if (! is_string($value)) {
81+
throw new Exception('Secret value cannot be empty');
82+
}
7083

7184
$brefCloud = new BrefCloudClient;
7285

src/Components/ServerlessFramework.php

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ public function deploy(int $deploymentId, string $environment, array $awsCredent
3232
if ($input->hasOption('force')) {
3333
$options[] = '--force';
3434
}
35-
if ($input->hasOption('config') && $input->getOption('config')) {
36-
$configFile = (string) $input->getOption('config');
35+
$configOption = $input->getOption('config');
36+
if ($input->hasOption('config') && is_string($configOption) && $configOption !== '') {
37+
$configFile = $configOption;
3738
$options[] = '--config';
3839
$options[] = $configFile;
3940
} else {
@@ -170,10 +171,11 @@ private function retrieveOutputs(string $oslsPackage, string $environment, array
170171
$url = substr($url, strlen('ANY - '));
171172
}
172173
if (!$url && isset($deployOutputs['endpoints']) && is_array($deployOutputs['endpoints'])) {
173-
$url = reset($deployOutputs['endpoints']);
174+
$firstEndpoint = reset($deployOutputs['endpoints']);
175+
$url = is_string($firstEndpoint) ? $firstEndpoint : null;
174176
}
175177
// Special case for the `server-side-website` construct
176-
if (isset($deployOutputs['website']['url']) && is_string($deployOutputs['website']['url'])) {
178+
if (isset($deployOutputs['website']) && is_array($deployOutputs['website']) && isset($deployOutputs['website']['url']) && is_string($deployOutputs['website']['url'])) {
177179
$url = $deployOutputs['website']['url'];
178180
}
179181
if (! isset($deployOutputs['Stack Outputs']) || ! is_array($deployOutputs['Stack Outputs'])) {
@@ -203,14 +205,7 @@ private function retrieveOutputs(string $oslsPackage, string $environment, array
203205
$stackOutputs = $this->cleanupCfOutputs($stackOutputs);
204206

205207
$stackName = $stackMatches[1];
206-
if (! is_string($stackName)) {
207-
throw new Exception('Invalid stack name in the "serverless info" output');
208-
}
209-
210208
$region = $regionMatches[1];
211-
if (! is_string($region)) {
212-
throw new Exception('Invalid region in the "serverless info" output');
213-
}
214209

215210
return array_merge([
216211
'stack' => $stackName,
@@ -249,17 +244,23 @@ private function serverlessExec(string $oslsPackage, string $command, string $en
249244
}
250245

251246
/**
252-
* @param array<string, string> $outputs
247+
* @param array<mixed, mixed> $outputs
253248
* @return array<string, string>
254249
*/
255250
private function cleanupCfOutputs(array $outputs): array
256251
{
257-
return array_filter($outputs, function (string $name): bool {
258-
if ($name === 'ServerlessDeploymentBucketName') return false;
259-
if ($name === 'HttpApiId') return false;
260-
if (str_contains($name, 'LambdaFunctionQualifiedArn')) return false;
261-
return true;
262-
}, ARRAY_FILTER_USE_KEY);
252+
$result = [];
253+
foreach ($outputs as $name => $value) {
254+
if (! is_string($name) || ! is_string($value)) {
255+
continue;
256+
}
257+
if ($name === 'ServerlessDeploymentBucketName') continue;
258+
if ($name === 'HttpApiId') continue;
259+
if (str_contains($name, 'LambdaFunctionQualifiedArn')) continue;
260+
$result[$name] = $value;
261+
}
262+
263+
return $result;
263264
}
264265

265266
private function findErrorMessageInServerlessOutput(string $entireSlsOutput): string

0 commit comments

Comments
 (0)