Conversation
d4486eb to
f1e61a7
Compare
Add phpstan/phpstan ^2 at level 5 with a baseline of 13 errors (all interface method gaps on ClientInterface and ConnectorInterface). Globally ignore new.static (deliberate polymorphism pattern). Add PHPStan step to GitHub Actions CI and a Makefile with lint/test targets. Fixes found by PHPStan: - Project::systemInformation() return type missing false case - RestoreOptions: redundant @return PHPDocs conflicting with native static return type - LogItem: static:: to self:: for private method - Tree::getBlob(): always-true instanceof and unreachable code - Resolver: always-true explode() assignment in condition - ResourceWithReferences: always-true getResponse() check - Result::getEntity(): redundant isset() on initialized property - Subscription::create(): new self() vs new static() return mismatch - EnvironmentTest: incorrect @var annotation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces PHPStan static analysis (level 5) with a baseline, integrates ECS + PHPStan into CI, and includes several small code/doc fixes to satisfy static analysis and style checks across the client and model layers.
Changes:
- Add PHPStan (
phpstan/phpstan ^2) configuration + baseline, plus CI steps and Makefile targets for linting/testing. - Update
ConnectorInterfaceand adjustPlatformClientto avoid deprecatedgetAccountsEndpoint()usage unless using the concreteConnector. - Apply a set of targeted code/doc tweaks across models to satisfy PHPStan/ECS (return types, dead code removal, doc conflicts).
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Model/EnvironmentTest.php | Removes a now-unneeded/undesired local array-shape annotation for test cases. |
| src/PlatformClient.php | Guards deprecated accounts-endpoint usage behind instanceof Connector; adjusts API URL fallback logic. |
| src/Model/Subscription.php | Uses new static(...) to match static return type for polymorphic construction. |
| src/Model/Settings.php | Adds strict_types and expands empty class body to satisfy ECS style. |
| src/Model/Result.php | Removes redundant isset() check on an initialized typed property. |
| src/Model/ResourceWithReferences.php | Simplifies BadResponseException handling to always use the response status code. |
| src/Model/Ref/Resolver.php | Simplifies explode() handling to satisfy static analysis. |
| src/Model/Project.php | Updates systemInformation() return type to include false. |
| src/Model/Git/Tree.php | Simplifies getBlob() control flow and removes dead code. |
| src/Model/Backups/RestoreOptions.php | Removes redundant PHPDocs that conflict with native static return types. |
| src/Model/AutoscalingSettings.php | Adds strict_types and expands empty class body to satisfy ECS style. |
| src/Model/ActivityLog/LogItem.php | Switches private decode calls to self:: and updates return PHPDocs. |
| src/Connection/ConnectorInterface.php | Adds getConfig() and getAccessToken() to the connector contract. |
| phpstan.neon | Adds PHPStan configuration (level 5), baseline include, and ignore for new.static. |
| phpstan-baseline.neon | Adds baseline ignores for known Guzzle ClientInterface shorthand-method findings. |
| composer.json | Adds phpstan/phpstan as a dev dependency. |
| composer.lock | Locks PHPStan dependency. |
| Makefile | Adds lint-ecs, lint-phpstan, and test targets. |
| .github/workflows/ci.yml | Runs ECS + PHPStan in CI before PHPUnit. |
Comments suppressed due to low confidence (2)
phpstan.neon:13
- phpstan.neon uses tab indentation, but .editorconfig sets indent_style=space for all files. This makes the file inconsistent with repository formatting rules and can cause noisy diffs/auto-formatting issues. Consider converting indentation to spaces in this file (and keeping it consistent with phpstan-baseline.neon too).
includes:
- phpstan-baseline.neon
parameters:
level: 5
paths:
- src
- tests
ignoreErrors:
# Deliberate pattern: resource classes use new static() for polymorphism.
-
identifier: new.static
src/Model/ActivityLog/LogItem.php:57
- LogItem::multipleFromJsonStream() still constructs items with
new static(...)(via static::singleFromJson()), but the updated PHPDoc says@return self[]. This is inconsistent with multipleFromJsonStreamWithSeal() (which documentsstatic[]) and can mislead static analysis/consumers if LogItem is ever subclassed. Consider usingstatic[]here (or switching construction tonew self(...)if the intent is to always return LogItem).
/**
* @return self[]
*@deprecated use LogItem::multipleFromJsonStreamWithSeal() instead
*/
public static function multipleFromJsonStream(string $str): array
{
$items = [];
foreach (explode("\n", trim($str, "\n")) as $line) {
if ($line === '') {
continue;
}
$item = static::singleFromJson($line);
if ($item !== false) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Get the connector configuration. | ||
| */ | ||
| public function getConfig(): array; | ||
|
|
||
| /** | ||
| * Returns the access token saved in the session, if any. | ||
| */ | ||
| public function getAccessToken(): ?string; | ||
|
|
There was a problem hiding this comment.
Adding new required methods to ConnectorInterface (getConfig(), getAccessToken()) is a backwards-incompatible change for any downstream code that implements this interface. If maintaining BC is important, consider introducing a new extended interface (e.g., ConfigAwareConnectorInterface) or keeping these methods off the interface and using instanceof/method_exists checks where needed.
| ignoreErrors: | ||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/Activity.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/Organization/Address.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 2 | ||
| path: src/Model/Organization/Organization.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/Organization/Organization.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/Organization/Profile.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/Ref/Resolver.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/SetupOptions.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/Team/Team.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/Model/UserAccess/ProjectUserAccess.php | ||
|
|
||
| - | ||
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:post\(\)\.$#' | ||
| identifier: method.notFound | ||
| count: 1 | ||
| path: src/PlatformClient.php |
There was a problem hiding this comment.
phpstan-baseline.neon uses tab indentation, but .editorconfig sets indent_style=space for all files. To keep formatting consistent (and avoid future whitespace churn), consider regenerating or reformatting the baseline using spaces.
| ignoreErrors: | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Activity.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Organization/Address.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 2 | |
| path: src/Model/Organization/Organization.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Organization/Organization.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Organization/Profile.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Ref/Resolver.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/SetupOptions.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Team/Team.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/UserAccess/ProjectUserAccess.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:post\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/PlatformClient.php | |
| ignoreErrors: | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Activity.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Organization/Address.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 2 | |
| path: src/Model/Organization/Organization.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Organization/Organization.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Organization/Profile.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Ref/Resolver.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/SetupOptions.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/Team/Team.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/Model/UserAccess/ProjectUserAccess.php | |
| - | |
| message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:post\(\)\.$#' | |
| identifier: method.notFound | |
| count: 1 | |
| path: src/PlatformClient.php |
Summary
phpstan/phpstan ^2at level 5 with a baseline of 11 errors (allClientInterface::get/patch/post()shorthand methods not on the Guzzle interface)new.static(deliberate polymorphism pattern across the resource model)lint-ecs,lint-phpstan, andtesttargetsPHPStan fixes
Project::systemInformation()return type missingfalsecaseRestoreOptions: remove redundant@returnPHPDocs conflicting with nativestaticreturn typeLogItem:static::toself::for private method callTree::getBlob(): reorder to guard clauses first, remove dead codeResolver: simplify always-trueexplode()assignment in conditionResourceWithReferences: remove always-truegetResponse()check (BadResponseExceptionalways has a response)Result::getEntity(): remove redundantisset()on initialized propertySubscription::create():new self()tonew static()to match return typeConnectorInterface
getConfig()andgetAccessToken()toConnectorInterfacegetAccountsEndpoint()calls behindinstanceof ConnectorECS
declare(strict_types=1), brace style, superfluous@returnTest plan
make lintpasses (ECS + PHPStan)make testpasses (PHPUnit)🤖 Generated with Claude Code