mspirkov/yii2-phpstan-rules

GitHub: mspirkov/yii2-phpstan-rules

一套为 Yii2 项目定制的 PHPStan 规则集,通过静态分析在编码阶段发现架构分层混乱、配置拼写错误及隐蔽的运行时隐患。

Stars: 13 | Forks: 1

Yii2 PHPStan rules

这是我为自己日常 Yii2 项目整理的一套 PHPStan 规则。它们主要检查一些我个人尽量避免的问题——控制器中堆积业务逻辑、在视图中进行数据库访问、在任何地方读写 `Yii::$app`,以及看似正常实则不然的模型 `rules()` 和组件 `behaviors()` 数组。根据我的经验,它们有助于让 Yii2 代码库更整洁、更易于维护,但它们只是我将个人观点转化成的检查规则,而非通用标准——请取你所需,忽略或禁用剩下的部分。 [![PHP](https://img.shields.io/badge/%3E%3D7.4-7A86B8.svg?style=for-the-badge&logo=php&logoColor=white&label=PHP)](https://www.php.net/releases/7_4_0.php) [![Yii 2.0.x](https://img.shields.io/badge/%3E%3D2.0.53-247BA0.svg?style=for-the-badge&logo=yii&logoColor=white&label=Yii)](https://github.com/yiisoft/yii2/tree/2.0.53) [![测试](https://img.shields.io/github/actions/workflow/status/mspirkov/yii2-phpstan-rules/ci.yml?branch=main&style=for-the-badge&logo=github&label=Tests)](https://github.com/mspirkov/yii2-phpstan-rules/actions/workflows/ci.yml) [![PHPStan](https://img.shields.io/github/actions/workflow/status/mspirkov/yii2-phpstan-rules/ci.yml?branch=main&style=for-the-badge&logo=github&label=PHPStan)](https://github.com/mspirkov/yii2-phpstan-rules/actions/workflows/ci.yml) [![覆盖率](https://img.shields.io/codecov/c/github/mspirkov/yii2-phpstan-rules.svg?branch=main&style=for-the-badge&logo=codecov&logoColor=white&label=Coverage)](https://codecov.io/github/mspirkov/yii2-phpstan-rules) ![PHPStan Level Max](https://img.shields.io/badge/Max-7A86B8.svg?style=for-the-badge&label=PHPStan%20Level) ## 包含内容 | 规则 | 捕获内容 | | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | [`activeRecordRelationValidation`](#active-record-relations-validation) | 无效的 `hasOne()` / `hasMany()` 链接属性,这些属性在当前或关联的 ActiveRecord 模型中不存在 | | [`componentBehaviorsValidation`](#component-behaviors-validation) | `yii\base\Component` 中格式错误或无效的 `behaviors()` —— 未知的 behavior 类、错误的配置键以及错误的选项类型 | | [`modelRulesValidation`](#model-validation-rules-validation) | `yii\base\Model` 中格式错误或无效的 `rules()` —— 未知的验证器、缺失必填选项、错误的正则表达式、未知属性等 | | [`modelAttributeLabelsValidation`](#model-attribute-labels-validation) | `yii\base\Model` 中目标属性不存在的 `attributeLabels()` 条目,或使用了空的属性名 | | [`noComplexControllerActions`](#complexity-limits) | 包含过多分支/循环的控制器操作 —— 应该放在 service 中的逻辑 | | [`noComplexActionClasses`](#complexity-limits) | 同上,但针对独立的 `yii\base\Action` 类 | | [`noControllerActionCallsViaThis`](#no-calling-actions-via-this) | 在控制器内部使用 `$this->actionFoo()` 而不是使用重定向或共享方法 | | [`noDbQueriesInControllers`](#no-database-access-outside-repositories) | 控制器中的直接 DB/ActiveRecord 访问 | | [`noDbQueriesInActions`](#no-database-access-outside-repositories) | `Action` 类中的直接 DB/ActiveRecord 访问 | | [`noDbQueriesInViews`](#no-database-access-outside-repositories) | 视图文件中的直接 DB/ActiveRecord 访问 | | [`noDynamicQueryWhere`](#no-dynamic-sql-strings) | 传递给 `Query::where()` / `andWhere()` 的字符串拼接条件 | | [`noForbiddenYiiAppProperties`](#taming-yiiapp) | 读取任意的 `yii\base\Application` 组件,包括 `Yii::$app->*` | | [`noYiiAppPropertyMutation`](#taming-yiiapp) | 写入 `yii\base\Application` 属性,包括 `setComponents()` | | [`noDirectSuperglobals`](#no-raw-superglobals) | 直接使用 `$_GET`、`$_POST`、`$_SESSION` 等 | 每条规则都自带其专属的 PHPStan 错误标识符(`mspirkovYii2Rules.*`),因此你可以精确地针对 `ignoreErrors` 进行设置,而不是屏蔽整条规则。 ## 安装 ``` php composer.phar require --dev mspirkov/yii2-phpstan-rules ``` 如果你的项目使用了 [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer),这些规则会被自动加载——无需其他操作。 否则,请在你的 `phpstan.neon` 中手动包含它们: ``` includes: - vendor/mspirkov/yii2-phpstan-rules/rules.neon ``` ## 配置 默认启用所有规则。你可以在 `parameters.mspirkovYii2Rules` 下关闭整套规则,或对单个规则进行调整: ``` parameters: mspirkovYii2Rules: # Component IDs treated as "the database" by the DB-access rules yiiAppDbProperties: - db # Thresholds for the complexity rules — exceeding any one flags the method actionComplexity: ifCount: 3 foreachCount: 0 forCount: 0 whileCount: 0 doWhileCount: 0 switchCount: 0 matchCount: 0 ternaryCount: 1 tryCatchCount: 1 # Yii application properties allowed to be read anywhere (e.g. request-agnostic settings) noForbiddenYiiAppProperties: allowedProperties: - id - name - charset - language - timeZone # Project-specific model validator aliases modelRulesValidation: customValidators: slug: app\validators\SlugValidator # Disable a single rule without touching the rest noDynamicQueryWhere: enabled: false ``` ## 规则 ### Active Record 关联验证 `hasOne()` 和 `hasMany()` 关联链接是普通的字符串数组:数组的键属于关联的 AR 类,而值属于当前的 AR 类。此规则会检查这些属性是否存在,包括通过 PHPDoc `@property` 声明的属性。 ``` /** * @property int $id * @property int $customer_id * @property int $shipping_address_id */ final class Order extends ActiveRecord { public function getShippingAddress(): ActiveQuery { // ✗ missing property "uuid" on Address return $this->hasOne(Address::class, ['uuid' => 'shipping_address_id']); } public function getItems(): ActiveQuery { // ✗ missing property "order_uuid" on Order return $this->hasMany(OrderItem::class, ['order_id' => 'order_uuid']); } public function getCustomer(): ActiveQuery { // ✓ return $this->hasOne(Customer::class, ['id' => 'customer_id']); } } /** * @property int $id */ final class Customer extends ActiveRecord { } /** * @property int $id */ final class Address extends ActiveRecord { } /** * @property int $id * @property int $order_id */ final class OrderItem extends ActiveRecord { } ``` ### 组件 behavior 验证 `Component::behaviors()` 使用 Yii 对象配置,因此拼写错误通常要到运行时才会被发现。此规则会检查 `yii\base\Component` 子类(包括模型)上静态可见的 behavior 定义:类字符串、`class` / `__class` 配置数组、直接的 `Behavior` 实例、未知的类、没有继承 `yii\base\Behavior` 的类、未知的配置选项,以及根据公共属性或 setter 推断出的选项值类型。 ``` public function behaviors(): array { return [ 'timestamp' => [ 'class' => TimestampBehavior::class, 'createdAtAtribute' => 'created_at', // ✗ typo — unknown option ], 'typecast' => [ 'class' => AttributeTypecastBehavior::class, 'attributeTypes' => [ 'views_count' => AttributeTypecastBehavior::TYPE_INTEGER, 'is_published' => AttributeTypecastBehavior::TYPE_BOOLEAN, ], 'typecastAfterValidate' => 1, // ✗ bool expected ], 'invalid' => stdClass::class, // ✗ not a yii\base\Behavior 'slug' => [ 'class' => SluggableBehavior::class, 'attribute' => 'title', // ✓ ], ]; } ``` ### 模型验证规则验证 `Model::rules()` 只是一个普通数组——如果你忘记了一个验证器的必填选项、写了一个无效的正则表达式、配置错了某个选项,或者指向了一个根本不存在的属性,PHP 永远不会提示你。对于每个规则条目解析出的验证器类型(如 `required`/`string`/`number`/`compare`/`date`/`match`/`in`/`unique`/`exist`/`file`/`image`/`ip`/`url` 这样的内置别名、自定义的 `Validator` 子类、配置过的项目别名,或内联的闭包/方法),此规则会根据该验证器实际接受和需要的参数,对选项数组进行静态检查。无法解析的验证器名称将被报告为错误;你可以在 `modelRulesValidation.customValidators` 下添加特定项目的别名: ``` public function rules(): array { return [ ['email', 'string', 'lenght' => 255], // ✗ typo — unknown option "lenght" for StringValidator ['code', 'match', 'pattern' => '/[/'], // ✗ invalid regular expression ['ip', 'ip', 'ipv4' => false, 'ipv6' => false], // ✗ disables both protocols ['message', 'string', 'max' => 'invalid'], // ✗ 'max' must be int|null ['status', 'someUnregisteredAlias'], // ✗ unknown validator ['name', 'string', 'max' => 255], // ✓ ]; } ``` 此规则还会检查每条规则索引 0 处的属性名(包括属性数组列表)在模型中是否实际存在,这与 `activeRecordRelationValidation` 检查关联链接的方式相同——必须是声明的属性或 PHPDoc `@property`。它只会对能解析为字面量或常量字符串的属性名进行报告;在运行时动态构建的任何内容都将被忽略。 ``` /** * @property string $email */ final class ContactModel extends Model { public $name; public function rules(): array { return [ ['name', 'required'], ['emial', 'required'], // ✗ typo — "emial" is not a property on ContactModel ['email', 'string'], // ✓ declared via @property ]; } } ``` ### 模型属性标签验证 `Model::attributeLabels()` 就像 `rules()` 一样容易出错——拼错的键会静默回退到默认的友好化属性名,而不是显示你的标签。此规则会检查每个键是否是模型上的现有属性(作为声明的属性或 PHPDoc `@property`,解析方式与 `modelRulesValidation` 相同)并且不能为空: ``` /** * @property string $email */ final class ContactModel extends Model { public $name; public function attributeLabels(): array { return [ 'name' => 'Name', 'emial' => 'E-mail', // ✗ typo — "emial" is not a property on ContactModel 'email' => 'E-mail', // ✓ declared via @property ]; } } ``` ### 复杂度限制 `noComplexControllerActions` 和 `noComplexActionClasses` 会统计控制器操作或 `Action::run()` 内部的 `if`、`foreach`、`for`、`while`、`do-while`、`switch`、`match`、三元表达式以及 `try/catch` 块。一旦超过任何配置的阈值,规则就会触发,并指出导致超限的具体构造: ``` // ✗ flagged: 4 `if` statements against a default limit of 3 public function actionCheckout(): string { if ($this->cart->isEmpty()) { /* ... */ } if (!$this->cart->hasPaymentMethod()) { /* ... */ } if ($this->cart->hasOutOfStockItems()) { /* ... */ } if ($this->cart->hasExpiredCoupon()) { /* ... */ } return $this->render('checkout', ['cart' => $this->cart]); } // ✓ the decision tree moves to a service, the action just orchestrates public function actionCheckout(): string { return $this->render('checkout', $this->checkoutService->process($this->cart)); } ``` ### 禁止通过 `$this` 调用操作 ``` // ✗ flagged: bypasses the action-resolution pipeline (filters, events, results) public function actionEdit(int $id): Response { return $this->actionView($id); } // ✓ redirect, or extract the shared part into a private method / service public function actionEdit(int $id): Response { return $this->redirect(['view', 'id' => $id]); } ``` ### 禁止在 repository 外部进行数据库访问 当在控制器、`Action` 或视图文件中出现 `ActiveRecord::find()`/`findOne()`/`save()`、`Yii::$app->db`、`Yii::$app->db->createCommand()`、创建或配置 `Query`、事务等操作时,该规则将会触发。 ``` // ✗ flagged in a view: queries the database instead of just rendering data where(['status' => 1])->all() as $post): ?> // ✓ the controller/action fetches the data, the view only renders it ``` `noDbQueriesInControllers` / `noDbQueriesInActions` 会促使你将相同的查询构建逻辑放入 repository 或 service 中。查询构建器的设置也会被计算在内:在这些层级中,`new Query()`、`$query->where()` 以及对 `Query` 对象的动态调用都会被视为直接的数据库访问。 ### 禁止动态 SQL 字符串 ``` // ✗ flagged: string-built condition, one step from SQL injection $query->where("status = $status"); $query->where('status = ' . $status); // ✓ array condition syntax — parameterized, and PHPStan can see the shape $query->where(['status' => $status]); ``` ### 驯服 `Yii::$app` 有两条规则可以防止应用对象成为一个可以从任何地方读取或重新分配任意属性的位置。它们会检查类型为 `yii\base\Application` 的表达式,因此相同的限制适用于直接使用 `Yii::$app`、持有它的变量以及显式的 `Application` 实例: ``` // ✗ noForbiddenYiiAppProperties: arbitrary component access $cache = Yii::$app->cache; $app = Yii::$app; $request = $app->request; $application = new Application($config); $session = $application->session; // ✗ noYiiAppPropertyMutation: mutating the container at runtime Yii::$app->params = []; Yii::$app->setComponents([...]); $app->language = 'ru-RU'; $application->setComponents([...]); // ✓ inject the component instead public function __construct(private CacheInterface $cache) {} ``` 一个简短的允许列表(默认为 `id`、`name`、`charset`、`language`、`timeZone`)在所有地方都保持可用,因为它们实际上是静态配置,而不是可注入的服务。 ### 禁止原始超全局变量 ``` // ✗ flagged, with the fix suggested in the error message $id = $_GET['id']; // ✓ read through the injected yii\web\Request instead $id = $this->request->get('id'); ``` 涵盖 `$_GET`、`$_POST`、`$_REQUEST`、`$_SESSION`、`$_COOKIE`、`$_FILES` 和 `$_SERVER`,每一个都会指向对应的 `yii\web\Request` / `Session` / `UploadedFile` API。
标签:ffuf, OpenVAS, PHP, PHPStan, SOC Prime, Yii2, 云安全监控, 代码规范, 开发工具, 静态分析