Download the PHP package mspirkov/yii2-phpstan-rules without Composer
On this page you can find all versions of the php package mspirkov/yii2-phpstan-rules. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download mspirkov/yii2-phpstan-rules
More information about mspirkov/yii2-phpstan-rules
Files in mspirkov/yii2-phpstan-rules
Package yii2-phpstan-rules
Short Description A set of PHPStan rules for projects using the Yii2 framework
License MIT
Informations about the package yii2-phpstan-rules
Yii2 PHPStan rules
A set of PHPStan rules for Yii2 projects that I put together for my own day-to-day work. Yii2 leans heavily on loosely-typed config arrays and magic properties/methods that PHPStan can't see through on its own, and on conventions — like keeping business logic and database access out of controllers and views — that are easy to drift from without anyone noticing. These rules catch both: they validate Yii2-specific config and structure statically, and they enforce the architectural boundaries and other code-quality checks I try to keep in a codebase. 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.
Support
If you like this project, give it a ⭐ on GitHub — it helps others discover it.
Installation
[!IMPORTANT]
It works better with the latest versions of PHP, Yii2, and PHPStan. The more up-to-date the versions are, the more accurate the analysis is.
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:
Configuration
All rules are on by default. Turn the whole set off, turn off just one of the two rule groups, or tune individual rules, under parameters.mspirkovYii2Rules:
Rules at a glance
Validation rules
Statically validate Yii2's loosely-typed config arrays and array-driven conventions — shapes PHPStan can't check on its own because they only take effect at runtime. Toggle all of them at once with enableValidationRules.
| Rule | Catches |
|---|---|
activeFormFieldValidation |
ActiveForm::field() calls targeting an attribute that is missing, read-only, or write-only on the given model |
activeQueryWithValidation |
with() / joinWith() / innerJoinWith() calls referencing a relation that doesn't exist on the queried ActiveRecord model |
activeRecordConditionValidation |
findOne() / findAll() / deleteAll() / updateAll() / updateAllCounters() WHERE conditions with an unknown attribute or a mismatched value type |
activeRecordRelationValidation |
Invalid hasOne() / hasMany() link properties that do not exist on the current or related ActiveRecord model |
activeRecordUpdateValuesValidation |
updateAll() / updateAllCounters() attribute or counter values with an unknown attribute or a mismatched value type |
baseObjectInstantiationValidation |
new on a yii\base\BaseObject subclass whose last constructor argument is a $config array, with bad config keys and bad option types |
behaviorAttributesValidation |
TimestampBehavior/BlameableBehavior/SluggableBehavior/AttributeTypecastBehavior/DateTimeBehavior options naming an unknown model attribute |
componentBehaviorsValidation |
Malformed or invalid behaviors() in yii\base\Component — unknown behavior classes, bad config keys, and bad option types |
controllerActionsValidation |
Malformed or invalid actions() in yii\base\Controller — unknown action classes, bad config keys, and bad option types |
htmlActiveAttributeValidation |
Html::activeInput() / activeTextInput() / etc. calls referencing an attribute that does not exist on the given model |
modelAttributeHintsValidation |
attributeHints() entries in yii\base\Model that target attributes that don't exist, or use an empty attribute name |
modelAttributeLabelsValidation |
attributeLabels() entries in yii\base\Model that target attributes that don't exist, or use an empty attribute name |
modelRulesValidation |
Malformed or invalid rules() in yii\base\Model — unknown validators, missing required options, bad regexes, unknown attributes, and more |
modelScenariosValidation |
scenarios() entries in yii\base\Model with an empty name, a non-array attribute list, or an unknown attribute |
queryConditionValidation |
where() / andWhere() / orWhere() operator-format conditions (in, between, like, etc.) with the wrong number of operands |
uploadedFileInstanceValidation |
UploadedFile::getInstance() / getInstances() calls referencing an attribute that does not exist on the given model |
widgetPropertiesValidation |
Unknown or mistyped option keys and bad option types in Widget::begin() / Widget::widget() config arrays |
yiiCreateObjectValidation |
Yii::createObject() config arrays missing class/__class, bad config keys, and bad option types |
Code quality rules
Catch architectural drift, complexity, and other code-quality issues that are easy to miss without anyone noticing — business logic and database access staying out of controllers and views, actions calling other actions directly, superglobals, dynamic SQL, an Application object that anything can read from or write to, and calls that are provably redundant.
| Rule | Catches |
|---|---|
noComplexActionClasses |
Standalone yii\base\Action classes with too much branching/looping — logic that belongs in a service |
noComplexControllerActions |
The same, for controller actions |
noControllerActionCallsViaThis |
$this->actionFoo() inside a controller instead of a redirect or shared method |
noDbQueriesInActions |
Direct DB/ActiveRecord access in Action classes |
noDbQueriesInControllers |
Direct DB/ActiveRecord access in controllers |
noDbQueriesInViews |
Direct DB/ActiveRecord access in view files |
noDirectSuperglobals |
Direct use of $_GET, $_POST, $_SESSION, etc. |
noDynamicQueryWhere |
String-concatenated conditions passed to Query::where() / andWhere() / orWhere() |
noForbiddenYiiAppProperties |
Reads of arbitrary yii\base\Application components, including Yii::$app->* |
noRedundantExistenceCheck |
Query::one() !== null / Query::count() compared against 0 or 1 where Query::exists() suffices |
noRedundantHtmlEncode |
Html::encode() calls whose argument is always a numeric-string |
noYiiAppPropertyMutation |
Writes to yii\base\Application properties, including setComponents() |
Rule reference
Validation rules
Active Form field validation
ActiveForm::field($model, $attribute) binds an editable input to the attribute: it reads the current value to render the input, and writes the submitted value back to the model on load(). This rule checks that the attribute is both readable and writable — a declared (non-readonly) property, a PHPDoc @property, or a matching getter/setter pair — and reports it whether it's missing entirely or only exists as read-only or write-only. yii\base\DynamicModel instances (and subclasses) are skipped entirely, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.
ActiveQuery with() validation
with(), joinWith(), and innerJoinWith() take relation names as plain strings, so a typo (or a relation that got renamed) silently returns no related data instead of failing. This rule checks that every relation name passed to these methods — including a joinWith()/innerJoinWith() alias ('orders o' or 'orders AS o') and a dotted sub-relation path ('orders.items') — resolves to an actual relation (a getXxx() method returning something compatible with yii\db\ActiveQueryInterface) on the queried model.
Validating a sub-relation requires knowing which model the parent relation points to. This rule can work that out two ways: from the relation getter's own @return ActiveQuery<T> PHPDoc, or from a @property-read T / @property-read T[] PHPDoc property of the same name on the model (the same resolution activeFormFieldValidation and friends already rely on). A relation whose target model can't be determined either way is still checked for existence at its own level, but any further sub-relation path past it is left unchecked rather than guessed at.
Active Record condition validation
findOne(), findAll(), and deleteAll() take a plain array condition (['attribute' => value], with an array value matched as an IN (...) condition) — and so does the second, condition argument of updateAll() / updateAllCounters(). Like attributeLabels() and scenarios(), this is never checked against the model until the query actually runs. This rule checks that every attribute name in a condition array exists on the queried ActiveRecord model (the same @property-aware resolution as activeRecordRelationValidation) and that its value's type is compatible with the attribute's declared type. Only array literals with a resolvable string key are checked; primary-key-only lookups (findOne(1), findOne([1, 2])) and dynamically-built condition arrays are left alone. A value implementing yii\db\ExpressionInterface (e.g. new Expression('NOW()')) is accepted for any attribute regardless of its declared type — yii\db\conditions\HashConditionBuilder builds it as raw SQL instead of type-casting it, and does so per-value inside an IN (...) array too.
Active Record relations validation
hasOne() and hasMany() relation links are plain string arrays: the array keys belong to the related AR class, and the values belong to the current AR class. This rule checks that those properties exist, including properties declared through PHPDoc @property.
Active Record update values validation
updateAll()'s attribute values and updateAllCounters()'s counter values are the other plain array these two methods take — the values written into the row, as opposed to the WHERE condition activeRecordConditionValidation checks. This rule checks that every attribute name exists on the ActiveRecord model and that its value's type is compatible with the attribute's declared type; unlike a condition, these values are written as-is, so (unlike activeRecordConditionValidation) an array value is not treated as an IN (...) shorthand and is always a type mismatch. As with a condition, a value implementing yii\db\ExpressionInterface is accepted for any attribute regardless of its declared type — yii\db\QueryBuilder::prepareUpdateSets() builds it as raw SQL instead of type-casting it.
BaseObject instantiation validation
yii\base\BaseObject::__construct($config = []) applies $config via Yii::configure($this, $config), the same mechanism Yii::createObject() uses to apply its own config array — so a typo'd key or wrong-typed value in a plain new SomeObject([...]) call is just as invisible to PHPStan as it is in a createObject() config array. This rule checks a new call the same way yiiCreateObjectValidation checks Yii::createObject(): config keys against the target class's writable properties, and literal values against their declared types. It only looks at classes extending yii\base\BaseObject, and only at a literal array passed as the constructor's last argument when that argument is exactly the one named $config — the Yii2 convention for opting into array-config construction. A subclass whose last parameter isn't named config (or is variadic) doesn't follow that convention, so its last argument is left alone.
Behavior attributes validation
TimestampBehavior, BlameableBehavior, SluggableBehavior, AttributeTypecastBehavior, and mspirkov/yii2-db's DateTimeBehavior all fill in specific model attributes on their own — createdAtAttribute/updatedAtAttribute, createdByAttribute/updatedByAttribute, attribute/slugAttribute, attributeTypes, and the attributes event map every AttributeBehavior subclass inherits — and none of that is checked against the model until the behavior actually runs. This rule checks that every attribute name these options reference (a literal string, or an array of them) actually exists on the model declaring behaviors(), the same @property-aware resolution modelRulesValidation and modelAttributeLabelsValidation use. yii\base\DynamicModel instances are skipped, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.
TimestampBehavior, BlameableBehavior, SluggableBehavior, and DateTimeBehavior all extend yii\behaviors\AttributeBehavior, so instead of (or alongside) their own shorthand options, any of them can be configured directly through the inherited attributes event map — and this rule checks that map's attribute names the same way, on whichever of the four (or a custom AttributeBehavior subclass) it appears on:
Component behaviors validation
Component::behaviors() uses Yii object configs, so typos usually wait until runtime. This rule checks statically visible behavior definitions on yii\base\Component subclasses, including models: classes that do not extend yii\base\Behavior, bad config keys, unknown config options, and option value types inferred from public properties or setters.
Controller actions validation
Controller::actions() shares the same object-config shape as Component::behaviors() — this rule checks statically visible action definitions on yii\base\Controller subclasses: classes that do not extend yii\base\Action, an empty action ID, bad config keys, unknown config options, and option value types inferred from public properties or setters.
Html active attribute validation
Html::activeInput(), activeTextInput(), and the rest of the active*() family (activeHiddenInput, activePasswordInput, activeFileInput, activeTextarea, activeRadio, activeCheckbox, activeDropDownList, activeListBox, activeCheckboxList, activeRadioList, activeLabel, activeHint) all take a model and a plain attribute-name string, the same as ActiveForm::field(). This rule checks that the attribute exists on the given model, the same @property-aware resolution used by activeFormFieldValidation and uploadedFileInstanceValidation. yii\base\DynamicModel instances are skipped, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.
Model attribute hints validation
Model::attributeHints() is just as easy to get wrong as attributeLabels() — a typo'd key silently means the hint is never shown for the intended attribute. This rule checks that every key is an existing property on the model (as a declared property or a PHPDoc @property, same resolution as modelRulesValidation) and isn't left empty — though the existence check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically:
Model attribute labels validation
Model::attributeLabels() is just as easy to get wrong as rules() — a typo'd key silently falls back to the default humanized attribute name instead of showing your label. This rule checks that every key is an existing property on the model (as a declared property or a PHPDoc @property, same resolution as modelRulesValidation) and isn't left empty — though the existence check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically:
Model validation rules validation
Model::rules() is just a plain array — PHP will never tell you that you forgot a validator's required option, wrote an invalid regex, misconfigured one of its options, or targeted an attribute that doesn't even exist. 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:
This rule also checks that the attribute names at index 0 of each rule (including array lists of attributes) actually exist on the model, the same way activeRecordRelationValidation checks relation links — as a declared property or a PHPDoc @property. It only reports on attribute names it can resolve to a literal or constant string; anything built dynamically at runtime is left alone. This check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.
Model scenarios validation
Model::scenarios() maps scenario names to the attributes active in them, and PHP won't tell you that a scenario name is empty, an attribute list isn't actually an array, or an attribute doesn't exist on the model — the same way modelAttributeLabelsValidation checks attributeLabels(). An attribute prefixed with ! (Yii's "unsafe" marker) is checked under its unprefixed name. The attribute-existence check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.
Query condition validation
Query::where() / andWhere() / orWhere() accept an "operator format" array ([operator, operand1, operand2, ...]), and Yii only discovers a missing operand at query-build time — each yii\db\conditions\*Condition::fromArrayDefinition() throws an InvalidArgumentException if its required operands aren't present. This rule checks the operand count against those same rules: not, between / not between, in / not in, like and its variants, and exists / not exists each need a specific minimum (or, for not, an exact) number of operands, and the standard comparison operators (=, !=, <>, >, >=, <, <=) need exactly 2, Yii's documented "arbitrary operator" case. and / or operands are recursed into, since they typically wrap further operator-format sub-conditions; yii\db\conditions\ConjunctionCondition itself never validates their count, but a zero-operand and/or can never produce a meaningful condition, so this rule still requires at least one. Any other operator string — a genuinely custom one registered via QueryBuilder::setConditionClasses() — is left unchecked rather than guessed at, and so is anything built dynamically or in hash format (['status' => 1], never operator-format to begin with).
UploadedFile instance validation
UploadedFile::getInstance($model, $attribute) and getInstances($model, $attribute) build the file input's name from $model and a plain attribute-name string, the same way ActiveForm::field() does — so a typo silently returns null (or an empty array) instead of the uploaded file. This rule checks that the attribute exists on the given model, the same @property-aware resolution used elsewhere (e.g. activeFormFieldValidation, modelAttributeLabelsValidation). yii\base\DynamicModel instances are skipped, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.
Widget properties validation
Widget::begin($config) / Widget::widget($config) configs are just arrays, like behaviors(), so a typo'd key or a wrong-typed value only fails once the widget renders. This rule checks config keys against the called widget's writable properties and literal values against their declared types.
Yii::createObject() validation
Yii::createObject()'s class / __class config array is declared as an open, all-optional PHPStan array shape (array{class?: class-string<T>, __class?: class-string<T>, ...}), so PHPStan itself already flags an unknown or wrong-typed class value — but it stays silent about a missing class/__class key entirely (just an unhelpful "unable to resolve the template type" note) and about every other key in the array, since ... accepts anything. This rule fills exactly those two gaps on calls to createObject() on Yii (or any class extending yii\BaseYii): a clear "must specify class or __class" message, plus config keys checked against the resolved class's writable properties and value types, the same way componentBehaviorsValidation checks behaviors. Callables (a Closure, or a [$target, 'method'] array) are left alone, since they are not object configs.
Code quality rules
Complexity limits
noComplexActionClasses and noComplexControllerActions 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:
No calling actions via $this
No database access outside repositories
Fires on self::find()/findOne()/save(), Yii::$app->db, Yii::$app->db->createCommand(), creating or configuring a Query, transactions, and friends — wherever they turn up in a controller, an Action, or a view file.
noDbQueriesInActions / noDbQueriesInControllers push the same query building into a repository or service instead. Query builder setup counts too: new Query(), $query->where(), and dynamic calls on a Query object are all treated as direct database access in these layers.
No raw superglobals
Covers $_GET, $_POST, $_REQUEST, $_SESSION, $_COOKIE, $_FILES, and $_SERVER, each pointing at the matching yii\web\Request / Session / UploadedFile API.
No dynamic SQL strings
Applies to where(), andWhere(), and orWhere() alike. The check is purely structural — it flags any interpolated or concatenated string passed as the condition, regardless of what the string contains (an IN (...) list is just as flagged as a plain = comparison) — and leaves the array condition syntax, including its ['in', 'column', $values] operator form, untouched.
No forbidden Yii::$app properties
Checks any expression typed as yii\base\Application, not just Yii::$app directly. A short allowlist (id, name, charset, language, timeZone by default) stays available everywhere since those are effectively static configuration, not injectable services.
No redundant existence check
Query::exists() runs a lighter SELECT EXISTS(...) query instead of fetching a row (one()) or counting every matching row (count()). This rule catches the common ways a record-existence check like this ends up written as one of those instead, on any expression typed as yii\db\QueryInterface / yii\db\ActiveQueryInterface — the comparison can be written with the query call on either side, and count() > 0 / count() !== 0 (or, negated, count() < 1 / count() === 0) are flagged the same way as one() !== null / one() === null:
No redundant Html::encode()
PHPStan already flags most nonsensical Html::encode() calls on its own (wrong argument types and the like). The one gap it doesn't cover is a numeric-string argument: a value PHPStan can already prove only ever holds digits, so escaping it can't do anything — htmlspecialchars() never touches a plain number. This rule fires only in that narrow case, on yii\helpers\Html / BaseHtml and their subclasses:
No Yii::$app property mutation
Checks the same yii\base\Application-typed expressions as noForbiddenYiiAppProperties, on the write side.