正如官方网站所说 - phpstan 扫描您的整个代码库,并寻找明显和棘手的错误。即使在那些肯定没有测试的语句肯定没有涵盖的语句中,也很少执行。
为了解决主题问题,我们将使用phpstan静态分析工具。它具有足够强大的引擎,使我们能够在存储某些业务逻辑的代码中找到所有控制器。这样,我们将迫使开发人员使用服务或动作。要实现此规则,您必须已经安装和配置了PHPSTAN。
让我提醒您,phpstan是静态代码分析仪。这意味着它不运行代码,而只是读取它。它执行许多称为规则的检查。例如,如果代码调用某些方法,它将检查其调用是否与该方法的参数匹配,并且这些参数与类型匹配。如果发现任何问题,该程序将在最终报告中报告。
您可以找到如何在其官方网站上安装phpstan的信息,但是基本步骤是:
- 通过作曲家安装包装:
composer require --dev phpstan/phpstan
- 在项目根部创建 phpstan.neon 文件
- Run PHPStan:
vendor/bin/phpstan
base的示例 phpstan.neon 文件:
parameters:
level: 9
paths:
- ./src/
请注意最大验证级别,即级别:9 要使所有您可以从此工具中脱颖而出并保持最大严格的验证 - 强烈建议使用此级别。
让我们继续写作规则。
PHPSTAN中的规则是实现PHPStan\Rules\Rule
接口的类。您可以阅读有关如何在official website page上编写自定义规则的信息。
<?php declare(strict_types=1);
namespace App\PHPStan\Rules;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\Rule
/**
* @implements Rule<Class_>
*/
class ProhibitBusinessLogicInController implements Rule
{
public function getNodeType(): string
{
return Stmt\Class_::class;
}
public function processNode(Node $node, Scope $scope): array
{
$className = (string) $node->namespacedName;
if (!$this->isController($className)) {
return [];
}
foreach ($node->getMethods() as $method) {
foreach ($method->getStmts() as $statement) {
if ($this->isStatementWithBusinessLogic($statement)) {
return [
RuleErrorBuilder::message(sprintf(
'Method "%s::%s" contains business logic. Do something better.',
$className,
$method->name->toString(),
))
->build(),
];
}
}
}
return [];
}
private function isController(string $class): bool
{
return str_ends_with($class, 'Controller');
}
private function isStatementWithBusinessLogic($statement): bool
{
return in_array($statement::class, [
Stmt\If_::class,
Stmt\For_::class,
Stmt\Foreach_::class,
Stmt\While_::class,
Stmt\Switch_::class,
]);
}
}
您可能会注意到,该规则应具有两种方法: getNodeType 和 ProcessNode 。它的工作原理与事件调度程序类似:您注册了感兴趣的事件类型,然后在发生事件时收到通知。对于您的规则,您注册了所需的节点类型,当PHPSTAN遇到此节点时,它称为 ProcessNode 方法。但是其中的“ 节点$ node ”到底是什么?
静态代码分析是指通过代码的每个元素(节点)的可能性。例如。类是一个单独的节点,每个类方法也是一个节点。每个关键字或表达式也是一个节点。每个PHP文件可以由某些节点的一棵大树表示。
phpstan内部使用php-parser库来解析PHP文件,并为每个文件创建一个AST(抽象语法树)。然后,它遍历这棵树,询问每个规则是否需要处理当前节点( getNodeType )。如果需要,它将通过节点进一步处理以进行处理( ProcessNode )。作为回报,我们会收到一系列错误。
在我们的示例中,我们将分析控制器,因此我们使用类_ 节点类型。
接下来的 ProcessNode 方法我们需要确定传递的类是控制器。毕竟,我们的规则只能与控制器和其他类别一起工作。
让我们尝试定义可以考虑哪个控制器的类。在这里,我们面临一个问题,因为它可能会因一个项目而异,而在这里,我正在尝试简化它,并使用一种简单的方法将控制器定义为Postfix“ Controller ”。
接下来,我们必须获取每个控制器方法的所有语句。如果与我们的禁止语句列表有关的语句(请参见 isstatementWithBusinessLogic ),则此方法包含业务逻辑。
要注册我们的规则,我们需要在 phpstan.neon.neon 文件中写下它:
services:
- class: App\PHPStan\Rules\ProhibitBusinessLogicInControllerRule
tags:
- phpstan.rules.rule
如果您喜欢TDD开发phpstan,则可以为您提供一个为您的自定义规则编写测试的好机会。 PHPSTAN规则使用Phpunit进行了测试。
自定义规则测试代码:
<?php declare(strict_types=1);
namespace Tests\PHPStan\Rules;
use App\PHPStan\Rules\ProhibitBusinessLogicInControllerRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
class ProhibitBusinessLogicInControllerRuleTest extends RuleTestCase
{
public function testSkipControllerWithNothingWrong(): void
{
$this->analyse(
[
__DIR__ . '/fixtures/skip-controller-if-everything-ok.php',
],
[]
);
}
public function testControllerContainsForLoop(): void
{
$this->analyse(
[
__DIR__ . '/fixtures/controller-with-for-loop.php',
],
[
['Method "WithForLoopController::indexAction" contains business logic. Do something better.', 4]
],
);
}
/**
* @inheritDoc
*/
protected function getRule(): Rule
{
return new ProhibitBusinessLogicInControllerRule();
}
}
您可以从上面的示例中看到所有测试接受固定装置并比较响应,无论是否返回错误。如果是,那么哪一个。我将举例说明固定文件 fixtures/controler-for-loop.php 。
<?php declare(strict_types=1);
class WithForLoopController
{
public function __construct(
private readonly Repository $repository
) { }
public function indexAction(): array
{
$result = [];
for ($i = 0; $i < 100; $i++) {
$result[] = $this->repository->getIndex($i);
}
return $result;
}
}
在此固定装置中,我们正在测试控制器中使用的循环的情况。请随时在测试中涵盖所有“ IsStatementWithBusinessLogic”案例。
因此,在本文中,我们为phpstan编写了一个相当简单但功能性的规则。我们已经讨论了Phpstan的基础知识,现在我们可以通过更复杂的测试来介绍我们的项目。我还建议阅读developer documentation。学习更多规则开发功能不会花很长时间。