定制 pepakriz/phpstan-exception-rules 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

pepakriz/phpstan-exception-rules

Composer 安装命令:

composer require pepakriz/phpstan-exception-rules

包简介

Exception rules for PHPStan

README 文档

README

Build Status Latest Stable Version License

This extension provides following rules and features:

  • Require @throws annotation when some checked exception is thrown (examples)
  • Exception propagation over:
    • Function calls
    • Magic, dynamic and static method calls
    • Iterable interface in foreach and in iterator_*() functions (examples)
    • Countable interface combinated with count() function (examples)
    • JsonSerializable interface combinated with json_encode() function (examples)
  • Ignore caught checked exceptions (examples)
  • Unnecessary @throws annotation detection (examples)
  • Useless @throws annotation detection (examples)
  • Optionally allows unused @throws annotations in subtypes (examples)
  • @throws annotation variance validation (examples)
  • Dynamic throw types based on arguments
  • Unreachable catch statements
    • exception has been caught in some previous catch statement (examples)
    • exception has been caught twice in the same catch statement (examples)
    • checked exception is never thrown in the corresponding try block (examples)
  • Report throwing checked exceptions in the global scope (examples)

Features and rules provided by PHPStan core (we rely on):

  • @throws annotation must contain only valid Throwable types
  • Thrown value must be subclass of Throwable

Usage

To use this extension, require it in Composer:

composer require --dev pepakriz/phpstan-exception-rules

And include and configure extension.neon in your project's PHPStan config:

includes:
	- vendor/pepakriz/phpstan-exception-rules/extension.neon

parameters:
	exceptionRules:
		reportUnusedCatchesOfUncheckedExceptions: false
		reportUnusedCheckedThrowsInSubtypes: false
		reportCheckedThrowsInGlobalScope: false
		checkedExceptions:
			- RuntimeException

You could use uncheckedExceptions when you prefer a list of unchecked exceptions instead. It is a safer variant, but harder to adapt to the existing project.

parameters:
	exceptionRules:
		uncheckedExceptions:
			- LogicException
			- PHPUnit\Framework\Exception

checkedExceptions and uncheckedExceptions cannot be configured at the same time

If some third-party code defines wrong throw types (or it doesn't use @throws annotations at all), you could override definitions like this:

parameters:
	exceptionRules:
		methodThrowTypeDeclarations:
			FooProject\SomeService:
				sendMessage:
					- FooProject\ConnectionTimeoutException
				methodWithoutException: []
		functionThrowTypeDeclarations:
			myFooFunction:
				- FooException

In some cases, you may want to ignore exception-related errors as per class basis, as is usually the case for testing:

parameters:
	exceptionRules:
		methodWhitelist:
			PHPUnit\Framework\TestCase: '#^(test|(setup|setupbeforeclass|teardown|teardownafterclass)$)#i'

Extensibility

Dynamic throw type extensions - If the throw type is not always the same, but depends on an argument passed to the method. (Similar feature as Dynamic return type extensions)

There are interfaces, which you can implement:

  • Pepakriz\PHPStanExceptionRules\DynamicMethodThrowTypeExtension - service tag: exceptionRules.dynamicMethodThrowTypeExtension
  • Pepakriz\PHPStanExceptionRules\DynamicStaticMethodThrowTypeExtension - service tag: exceptionRules.dynamicStaticMethodThrowTypeExtension
  • Pepakriz\PHPStanExceptionRules\DynamicConstructorThrowTypeExtension - service tag: exceptionRules.dynamicConstructorThrowTypeExtension
  • Pepakriz\PHPStanExceptionRules\DynamicFunctionThrowTypeExtension - service tag: exceptionRules.dynamicFunctionThrowTypeExtension

and register as service with correct tag:

services:
	-
		class: App\PHPStan\EntityManagerDynamicMethodThrowTypeExtension
		tags:
			- exceptionRules.dynamicMethodThrowTypeExtension

Motivation

There are 2 types of exceptions:

  1. Safety-checks that something should never happen (you should never call some method in some case etc.). We call these LogicException and if they are thrown, programmer did something wrong. For that reason, it is important that this exception is never caught and kills the application. Also, it is important to write good descriptive message of what went wrong and how to fix it - that is why every LogicException must have a message. Therefore, inheriting LogicException does not make much sense. Also, LogicException should never be @throws annotation (see below).
  2. Special cases in business logic which should be handled by application and error cases that just may happen no matter how hard we try (e.g. HTTP request may fail). These exceptions we called RuntimeException or maybe better "checked exception". All these exceptions should be checked. Therefore it must be either caught or written in @throws annotation. Also if you call an method with that annotation and do not catch the exception, you must propagate it in your @throws annotation. This, of course, may spread quickly. When this exception is handled (caught), it is important for programmer to immediately know what case is handled and therefore all used RuntimeExceptions are inherited from some parent and have very descriptive class name (so that you can see it in catch construct) - for example CannotCloseAccountWithPositiveBalanceException. The message is not that important since you should always catch these exceptions somewhere, but in our case we often use that message in API output and display it to end-user, so please use something informative for users in that cases (you can pass custom arguments to constructor (e.g. entities) to provide better message). Sometimes you can meet a place where you know that some exception will never be thrown - in this case you can catch it and wrap to LogicException (because when it is thrown, it is a programmer's fault).

It is always a good idea to wrap previous exception so that we do not lose information of what really happened in some logs.

// no throws annotation
public function decide(int $arg): void
{
	switch ($arg) {
		case self::ONE:
			$this->decided()
		case self::TWO:
			$this->decidedDifferently()
		default:
			throw new LogicException("Decision cannot be made for argument $arg because of ...");
	}
}

/**
 * @return mixed[]
 *
 * @throws PrintJobFailedException
 */
private function sendRequest(Request $request): array
{
	try {
		$response = $this->httpClient->send($request);
		return Json::decode((string) $response->getBody(), Json::FORCE_ARRAY);

	} catch (GuzzleException | JsonException $e) {
		throw new PrintJobFailedException($e);
	}
}

class PrintJobFailedException extends RuntimeException
{

	public function __construct(Throwable $previous)
	{
		parent::__construct('Printing failed, remote printing service is down. Please try again later', $previous);
	}

}

Known limitations

Anonymous functions are analyzed at the same place they are declared

False positive when a method does not execute declared function:

/**
 * @throws FooRuntimeException false positive
 */
public function createFnFoo(int $arg): callable
{
	return function () {
		throw new FooRuntimeException();
	};
}

But most of use-cases just works:

/**
 * @param string[] $rows
 * @return string[]
 *
 * @throws EmptyLineException
 */
public function normalizeRows(array $rows): array
{
	return array_map(function (string $row): string {
		$row = trim($row);
		if ($row === '') {
			throw new EmptyLineException();
		}

		return $row;
	}, $rows);
}

Catch statement does not know about runtime subtypes

This case is detected by rule, so you will be warned about a potential risk.

Runtime exception is absorbed:

// @throws phpdoc is not required
public function methodWithoutThrowsPhpDoc(): void
{
	try {
		throw new RuntimeException();
		$this->dangerousCall();

	} catch (Throwable $e) {
		throw $e;
	}
}

As a workaround you could use custom catch statement:

/**
 * @throws RuntimeException
 */
public function methodWithThrowsPhpDoc(): void
{
	try {
		throw new RuntimeException();
		$this->dangerousCall();

	} catch (RuntimeException $e) {
		throw $e;
	} catch (Throwable $e) {
		throw $e;
	}
}

pepakriz/phpstan-exception-rules 适用场景与选型建议

pepakriz/phpstan-exception-rules 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 2.3M 次下载、GitHub Stars 达 109, 最近一次更新时间为 2018 年 05 月 08 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 pepakriz/phpstan-exception-rules 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 pepakriz/phpstan-exception-rules 我们能提供哪些服务?
定制开发 / 二次开发

基于 pepakriz/phpstan-exception-rules 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 2.3M
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 111
  • 点击次数: 20
  • 依赖项目数: 32
  • 推荐数: 0

GitHub 信息

  • Stars: 109
  • Watchers: 5
  • Forks: 10
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2018-05-08