Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pending PHPStan errors in PR #233 #234

Closed
wants to merge 3 commits into from

Conversation

Ndiritu
Copy link

@Ndiritu Ndiritu commented Nov 7, 2023

Q A
Bug fix? fix #235
New feature? no
BC breaks? no
Deprecations? no
Documentation if this is a new feature, link to pull request in https://github.com/php-http/documentation that adds relevant documentation
License MIT

What's in this PR?

  • Fixes PHPStan failures in #233 by adding appropriate generic types to Promise PHPDocs
  • Removes previous PHPStan error suppressions that are resolved.

To Do

@Ndiritu Ndiritu changed the title fix pending PHPStan errors Fix pending PHPStan errors in https://github.com/php-http/client-common/pull/233 Nov 7, 2023
@Ndiritu Ndiritu changed the title Fix pending PHPStan errors in https://github.com/php-http/client-common/pull/233 Fix pending PHPStan errors in [#233](https://github.com/php-http/client-common/pull/233) Nov 7, 2023
@Ndiritu Ndiritu changed the title Fix pending PHPStan errors in [#233](https://github.com/php-http/client-common/pull/233) Fix pending PHPStan errors in PR #233 Nov 7, 2023
Copy link
Contributor

@dbu dbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

@dbu
Copy link
Contributor

dbu commented Nov 7, 2023

lets ignore the cs errors here, they will be fixed in #233

@dbu
Copy link
Contributor

dbu commented Nov 8, 2023

i tagged https://github.com/php-http/promise/releases/tag/1.2.1 and restarted the phpstan build. there are still some failures 🤔

@Ndiritu
Copy link
Author

Ndiritu commented Nov 8, 2023

i tagged https://github.com/php-http/promise/releases/tag/1.2.1 and restarted the phpstan build. there are still some failures 🤔

Taking a look.

@Ndiritu
Copy link
Author

Ndiritu commented Nov 15, 2023

@dbu looking into the failures related to the $onRejected callable.

PHPStan seems to have some limitations in recognizing that a ClientExceptionInterface is still a Throwable. Not recognizing the inheritance structure seems to only be an issue when using callable([type]): [type] syntax. (Looking for a relevant issue on the PHPStan repo to link here or creating one myself)

  • I'm proposing that we ignore the ClientExceptionInterface errors in this lib.
  • While testing the callable syntax I've outline the impact of expanding the parameter type for $onRejected to \Throwable for a developer who may have previously been expecting an \Exception and is using PHPStan.

Please see the link below.

https://phpstan.org/r/ab8a6122-a065-4f37-8b65-b66633d212bf

@dbu
Copy link
Contributor

dbu commented Nov 22, 2023

thanks a lot for looking into this!

that would be this interface, right? https://github.com/php-fig/http-client/blob/master/src/ClientExceptionInterface.php says it extends \Throwable. is it possible to narrow down to a more specific interface in this context?

if even we can't make it work correctly, we should revert the annotations on the promise. otherwise everybody has to ignore the errors and everybody will complain first.

have you been able to check with the phpstan authors if your example should work? they are really good at improving phpstan, if it makes sense they can maybe fix it soonish.

@dbu dbu mentioned this pull request Nov 22, 2023
@ste93cry
Copy link

ste93cry commented Nov 23, 2023

I reasoned a bit on the linked playground, and I think that PHPStan is right (btw, the same error is reported by Psalm). What's happenning is that we're declaring in the interface that $onRejected can accept any Throwable. However, in the implementation, we are narrowing the argument type to be a specific exception class: that's not possible because it would mean crashing the script as soon as something is passed to it that is neither an instance of that class nor a subclass, so the tool reports it correctly.

@Ndiritu
Copy link
Author

Ndiritu commented Nov 23, 2023

Thank you for breaking this down @ste93cry! I get it now.
Would it be correct to say that the ideal fix then is to update this lib's onRejected parameter types to \Throwable instead of ClientExceptionInterface? Which would not be a breaking change if I understand correctly

@Ndiritu
Copy link
Author

Ndiritu commented Nov 23, 2023

Alternatively, reverting the onRejected PHPDoc for the Promise interface to callable|null to prevent developers having to make changes. And probably specifying \Throwable in a major rev of the Promise lib?

https://phpstan.org/r/4ea07ab8-ccd1-4354-8373-e14eb14231f4

Copy link
Contributor

@dbu dbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed https://github.com/php-http/promise/blob/7fa2284a91241e7fa06976369e998933d510da1b/src/Promise.php#L42C1-L42C88 is a BC break (in terms of phpdoc). we can't narrow down what type of exception the callable must accept. i think the correct thing would be to have a second covariant for the exception of the promise. then we can correctly model that the promise can only ever throw ClientExceptionInterface

can you try that?

and we should adjust the HttpAsyncClient in the httplug repository to declare what promise it returns, then we don't need to change all the various async client implementations here.

@@ -122,6 +130,9 @@ public function reject(ClientExceptionInterface $exception): void
}
}

/**
* {@inheritDoc}
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't do inheritdoc when there is nothing else. if there is no docblock, most tools take the docblock of the parent/interface

@@ -90,6 +97,7 @@ public function getState(): string

/**
* Resolve this deferred with a Response.
* @param ResponseInterface $response
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't add phpdoc when it does not add information

Suggested change
* @param ResponseInterface $response

@@ -51,6 +53,11 @@ public function __construct(callable $waitCallback)
$this->onRejectedCallbacks = [];
}

/**
* @param callable(ResponseInterface): ResponseInterface|null $onFulfilled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be necessary with the @implements in the class docblock, it should come automatically from the covariant in the interface.

@@ -23,6 +23,7 @@ abstract public function sendRequest(RequestInterface $request): ResponseInterfa

/**
* @see HttpAsyncClient::sendAsyncRequest
* @return \Http\Promise\Promise<ResponseInterface|\Throwable>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add in interface instead

@@ -52,6 +53,9 @@ public function addHttpClient($client): void
*/
abstract protected function chooseHttpClient(): HttpClientPoolItem;

/**
* @return Promise<ResponseInterface>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed when HttpAsyncClient defines the return type

@@ -89,6 +90,9 @@ public function sendRequest(RequestInterface $request): ResponseInterface
return $response;
}

/**
* @return Promise<ResponseInterface>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed when HttpAsyncClient defines the return type

@@ -26,6 +27,9 @@ public function sendRequest(RequestInterface $request): ResponseInterface
return $this->chooseHttpClient($request)->sendRequest($request);
}

/**
* @return Promise<ResponseInterface>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed when HttpAsyncClient defines the return type

@@ -84,6 +84,9 @@ public function sendRequest(RequestInterface $request): ResponseInterface
return $pluginChain($request)->wait();
}

/**
* @return Promise<ResponseInterface>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed when HttpAsyncClient defines the return type

@@ -21,6 +23,7 @@ trait HttpAsyncClientDecorator

/**
* @see HttpAsyncClient::sendAsyncRequest
* @return Promise<ResponseInterface>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to fix this in the HttpAsyncClient instead. then it will be inferred for all the various implementations here:

https://github.com/php-http/httplug/blob/625ad742c360c8ac580fcc647a1541d29e257f67/src/HttpAsyncClient.php#L20

@ste93cry
Copy link

Would it be correct to say that the ideal fix then is to update this lib's onRejected parameter types to \Throwable instead of ClientExceptionInterface? Which would not be a breaking change if I understand correctly

Well, technically speaking it is a breaking change. You can narrow the return type in a child class because the declaration of the method is compatible, but you cannot narrow the type of an argument in a child class, because that would mean that the implementation cannot handle something that the parent declares it can. For this reason, since adding the typehint would mean that people have to change their child classes to take it into account, the same is true when adding the typehint in the DocBlock and then using a static analysis tool.

then we can correctly model that the promise can only ever throw ClientExceptionInterface

Couldn't the plugin also throw a ServerExceptionInterface?

@dbu
Copy link
Contributor

dbu commented Nov 24, 2023

there is no ServerExceptionInterface in PSR-18, the client exception is short to mean "http client exception" and psr18 says that every exception thrown must be a client exception. the async client follows the same logic in https://github.com/php-http/httplug/blob/2.x/src/HttpAsyncClient.php (except that for BC it has https://github.com/php-http/httplug/blob/2.x/src/Exception.php)

imo the correct solution is to make the exception customizable in the promise as well and in async client customize the promise to use Http\Client\Exception as the phpdoc text already explains but could not technically specify.

@ste93cry
Copy link

I wonder whether it makes sense to insist on trying to typehint onRejected more than now: the static analysis tool cannot infer which exception is thrown from it, so it cannot validate anyway that the next promise in the chain will receive a ClientExceptionInterface.

@dbu
Copy link
Contributor

dbu commented Nov 25, 2023

i think for documentation purposes its good to be clear what kind of exception may be thrown / has to be handled.

@ste93cry
Copy link

ste93cry commented Nov 26, 2023

I came up with this solution that seems to work fine 🎉Of course, the $onRejected typehint is not checked, but PHPStan does not complain at least. I think it's the best we can do with the current tools.

@dbu
Copy link
Contributor

dbu commented Dec 1, 2023

I played around in the phpstan playground and came up with this: https://phpstan.org/r/6ec99fad-fd67-4f64-ac0d-ba135e207700

The upside is that the async client can declare what type of exception the onRejected callback must expect, to match with the current text description in phpdoc and the implementations of adapters.

any thoughts on that? i plan to implement this soonish if no objections come up.

@dbu dbu deleted the branch php-http:fix-static-ci January 4, 2024 19:09
@dbu dbu closed this Jan 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants