mspirkov/yii2-phpstan-rules
Composer 安装命令:
composer require --dev mspirkov/yii2-phpstan-rules
包简介
A set of PHPStan rules for projects using the Yii2 framework
README 文档
README
Yii2 PHPStan rules
A set of PHPStan rules for Yii2 projects that I put together for my own day-to-day work. They check for a handful of things I personally try to avoid — business logic piling up in controllers, database access in views, Yii::$app being read and written from anywhere, model rules() arrays that look fine but aren't. In my experience they help keep a Yii2 codebase a bit cleaner and more maintainable, but they're just my opinions turned into checks, not a universal standard — use what's useful, ignore or disable the rest.
What's inside
| Rule | Catches |
|---|---|
noComplexControllerActions |
Controller actions with too much branching/looping — logic that belongs in a service |
noComplexActionClasses |
The same, for standalone yii\base\Action classes |
noControllerActionCallsViaThis |
$this->actionFoo() inside a controller instead of a redirect or shared method |
noDbQueriesInControllers |
Direct DB/ActiveRecord access in controllers |
noDbQueriesInActions |
Direct DB/ActiveRecord access in Action classes |
noDbQueriesInViews |
Direct DB/ActiveRecord access in view files |
noDynamicQueryWhere |
String-concatenated conditions passed to Query::where() / andWhere() |
noForbiddenYiiAppProperties |
Reads of arbitrary Yii::$app->* components |
noYiiAppPropertyMutation |
Writes to Yii::$app properties, including setComponents() |
noDirectSuperglobals |
Direct use of $_GET, $_POST, $_SESSION, etc. |
modelRulesValidation |
Malformed or invalid rules() in yii\base\Model — unknown validators, missing required options, bad regexes, and more |
Every rule ships with its own PHPStan error identifier (mspirkovYii2Rules.*), so you can target ignoreErrors precisely instead of silencing a whole rule.
Installation
php composer.phar require --dev mspirkov/yii2-phpstan-rules
If your project uses phpstan/extension-installer, the rules are picked up automatically — nothing else to do.
Otherwise, include them manually in your phpstan.neon:
includes: - vendor/mspirkov/yii2-phpstan-rules/rules.neon
Configuration
All rules are on by default. Turn the whole set off, or tune individual rules, under parameters.mspirkovYii2Rules:
parameters: mspirkovYii2Rules: enableAllRules: true # 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::$app 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
The rules
Complexity limits
noComplexControllerActions and noComplexActionClasses count if, foreach, for, while, do-while, switch, match, ternaries, and try/catch blocks inside a controller action or Action::run(). Cross any configured threshold and the rule fires, pointing at the exact construct that pushed it over:
// ✗ flagged: 4 `if` statements against a default limit of 3 public function actionCheckout() { if ($cart->isEmpty()) { /* ... */ } if (!$user->hasPaymentMethod()) { /* ... */ } if ($stock->isLow($cart)) { /* ... */ } if ($coupon->isExpired()) { /* ... */ } } // ✓ the decision tree moves to a service, the action just orchestrates public function actionCheckout() { $this->checkoutService->process($cart, $user); }
No calling actions via $this
// ✗ flagged: bypasses the action-resolution pipeline (filters, events, results) public function actionEdit($id) { // ... return $this->actionView($id); } // ✓ redirect, or extract the shared part into a private method / service public function actionEdit($id) { return $this->redirect(['view', 'id' => $id]); }
No database access outside repositories
Fires on ActiveRecord::find()/findOne()/save(), Yii::$app->db->createCommand(), Query::all()/one()/count(), transactions, and friends — wherever they turn up in a controller, an Action, or a view file.
// ✗ flagged in a view <?php foreach (Post::find()->where(['status' => 1])->all() as $post): ?> // ✓ the controller/action fetches the data, the view only renders it <?php foreach ($posts as $post): ?>
noDbQueriesInControllers / noDbQueriesInActions push the same query building into a repository or service instead.
No dynamic SQL strings
// ✗ 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]);
Taming Yii::$app
Two rules keep the service locator from becoming a place where any property can be read or reassigned from anywhere:
// ✗ noForbiddenYiiAppProperties: arbitrary component access $cache = Yii::$app->cache; // ✗ noYiiAppPropertyMutation: mutating the container at runtime Yii::$app->params = []; Yii::$app->setComponents([...]); // ✓ inject the component instead public function __construct(private CacheInterface $cache) {}
A short allowlist (id, name, charset, language, timeZone by default) stays available everywhere since those are effectively static configuration, not injectable services.
No raw superglobals
// ✗ flagged, with the fix suggested in the error message $id = $_GET['id']; // ✓ $id = Yii::$app->request->getQueryParam('id');
Covers $_GET, $_POST, $_REQUEST, $_SESSION, $_COOKIE, $_FILES, and $_SERVER, each pointing at the matching yii\web\Request / Session / UploadedFile API.
Model validation rules that lie
Model::rules() is just a plain array — PHP will never tell you that you forgot a validator's required option, wrote an invalid regex, or misconfigured one of its options. For every rule entry the validator type resolves to (a built-in alias like required/string/number/compare/date/match/in/unique/exist/file/image/ip/url, a custom Validator subclass, a configured project alias, or an inline closure/method), this rule statically checks the option array against what that validator actually accepts and requires. A validator name it can't resolve is reported as an error; add project-specific aliases under modelRulesValidation.customValidators:
public function rules() { return [ ['email', 'exist', 'targetClass' => X::class], // ✗ 'exist' requires 'targetAttribute' ['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
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 4
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-04