编写自定义phpstan规则以禁止控制器中的业务逻辑
#php #测试 #codequality #phpstan

正如官方网站所说 - 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。学习更多规则开发功能不会花很长时间。