Summary
Currently the rector AnnotateThrowablesRector only supports a direct thrown \Throwable like:
The statement throw is analyzed both in methods and in functions.
However, this is not sufficient to cover the full range of concrete possibilities that may lead to the throw of a \Throwable.
Following there are the possible kinds of \Throwables and the places in which is possible to find them.
1. Kinds of \Throwables (direct use of throw)
There are basically three types of possible thrown \Throwables:
- Direct class instantiation;
- Use of a variable;
- Use of a factory method.
1.1 - Direct class instantiation
How to proceed
This is the current covered type of \Throwable.
1.2 - Use of a variable
Example from a real world application that uses Guzzle and has to manage its exceptions
class VariableThrow
{
/**
* @param Throwable $e
*
* @throws Throwable
* @throws \InvalidArgumentException
*
* @return string
*/
public static function guessCode(Throwable $e): string
{
if ( ! $e instanceof Exception) {
throw new InvalidArgumentException('Only an \Exception can be passed.');
}
// This is thrown if there is an error in connection and relates to problem of the app itself
if ($e instanceof ConnectException) {
switch ($e->getHandlerContext()['errno']) {
case CURLE_OPERATION_TIMEOUTED:
return self::ERROR_CODE_OPERATION_TIMEOUT;
case CURLE_COULDNT_RESOLVE_HOST:
return self::ERROR_CODE_COULDNT_RESOLVE_HOST;
case CURLE_COULDNT_CONNECT:
return self::ERROR_CODE_COULDNT_CONNECT;
case CURLE_SSL_CONNECT_ERROR:
return self::ERROR_CODE_SSL_CONNECT_ERROR;
case CURLE_GOT_NOTHING:
return self::ERROR_CODE_GOT_NOTHING;
}
} elseif ($e instanceof TooManyRedirectsException) {
return self::ERROR_CODE_TOO_MANY_REDIRECTS;
} elseif ($e instanceof ClientException) {
return self::ERROR_CODE_CLIENT_4XX;
} elseif ($e instanceof ServerException) {
return self::ERROR_CODE_CLIENT_5XX;
}
// This is reached if the Exception kind is not handled: it has to be handled
elseif ($e instanceof GuzzleException) {
return self::ERROR_CODE_GUZZLE_ERROR;
}
// As last chance, rethrow the exception as it doesn't come from Guzzle
throw $e;
}
}
How to proceed
- Find all the possible values of
$e
- If they are classes, add them to the Docblock of method
VariableThrow::guessCode()
1.3 - Use of a factory method
<?php
class ThrowWithAFactoryMethod
{
/**
* @throws Exception
*/
private function createException()
{
return new \Exception();
}
/**
* @throws Exception
*/
public function callingMethod()
{
throw $this->createException();
}
}
How to proceed
- Access the Dockblock of
ThrowWithAFactoryMethod::createException()
- Get the
@return tags of the method
- Add the found
@return tags to Docblock of method MethodCall::callingMethod(), BUT as @throws tags
2. Scenarios in which is possible to find a throw of a \Throwable
- Method call
- Function call
2.1 - Method call
<?php
class MethodCall
{
/**
* @throws Exception
*/
private function methodThatThrows()
{
throw new \Exception();
}
/**
* @throws Exception
*/
public function callingMethod()
{
$this->methodThatThrows();
}
}
2.2 - Function call
This case can be easily illustrated using thecodingmachine/safe library:
<?php
use function Safe\sprintf;
class UseOfAPossibleThrow
{
/**
* @return string
* @throws \Safe\Exceptions\StringsException
*/
public function callingMethod()
{
return sprintf('On error, this call will throw an exception of kind %s', \Safe\Exceptions\StringsException::class);
}
}
How to proceed
The process is the same for both methods and functions.
Given a code like this:
public function aFunction(string $aParam)
{
if (false === $aParam) {
throw new \Exception();
}
$defaultException = new \RuntimeException();
switch($param) {
case 1:
return a_function_that_may_throw_an_exception();
case 2:
return $object->aMethodThatMayThrowAnException();
case 3:
$defaultException = new AnotherException();
}
throw $defaultException;
}
- Analyze all
throws
- If they are a class, annotate them
- If they are a variable (
$defaultException), collect all the possible values it can assume
1. For each collected value, if is a class, annotate it
- Create a list of all function calls and for each of them
- Access the Dockblock of function
sprintf
- Get the
@throws tags of the function
- Add the found
@throws tags to Docblock of method UseOfAPossibleThrow::callingMethod()
- Create a list of all method calls and for each of them
- Access the Dockblock of function
sprintf
- Get the
@throws tags of the function
- Add the found
@throws tags to Docblock of method UseOfAPossibleThrow::callingMethod()
Implementation problems
The main problem is that it seems that currently it is not possible to access the Docblock of a method or of a function from the Throw_ PhpParser node.
Without being able to access the Docblock, it is not possible to annotate the calling method.
Maybe we need a new NodeVisitor or we need to improve someone of the already existing?
https://github.com/nikic/PHP-Parser/blob/master/doc/2_Usage_of_basic_components.markdown#node-traversation
@TomasVotruba , please, RFC.
Summary
Currently the rector
AnnotateThrowablesRectoronly supports a direct thrown\Throwablelike:The statement
throwis analyzed both in methods and in functions.However, this is not sufficient to cover the full range of concrete possibilities that may lead to the throw of a
\Throwable.Following there are the possible kinds of
\Throwablesand the places in which is possible to find them.1. Kinds of
\Throwables(direct use ofthrow)There are basically three types of possible thrown
\Throwables:1.1 - Direct class instantiation
How to proceed
This is the current covered type of
\Throwable.1.2 - Use of a variable
Example from a real world application that uses Guzzle and has to manage its exceptions
How to proceed
$eVariableThrow::guessCode()1.3 - Use of a factory method
How to proceed
ThrowWithAFactoryMethod::createException()@returntags of the method@returntags to Docblock of methodMethodCall::callingMethod(), BUT as@throwstags2. Scenarios in which is possible to find a throw of a
\Throwable2.1 - Method call
2.2 - Function call
This case can be easily illustrated using
thecodingmachine/safelibrary:How to proceed
The process is the same for both methods and functions.
Given a code like this:
throws$defaultException), collect all the possible values it can assume1. For each collected value, if is a class, annotate it
sprintf@throwstags of the function@throwstags to Docblock of methodUseOfAPossibleThrow::callingMethod()sprintf@throwstags of the function@throwstags to Docblock of methodUseOfAPossibleThrow::callingMethod()Implementation problems
Not yet analyzed in depthImplementedHow to access the Dockblock of the factory method from theImplementedThrow_node?The main problem is that it seems that currently it is not possible to access the Docblock of a method or of a function from the
Throw_PhpParser node.Without being able to access the Docblock, it is not possible to annotate the calling method.
Maybe we need a new
NodeVisitoror we need to improve someone of the already existing?https://github.com/nikic/PHP-Parser/blob/master/doc/2_Usage_of_basic_components.markdown#node-traversation
@TomasVotruba , please, RFC.