PHPackages                             mspirkov/yii2-phpstan-rules - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. mspirkov/yii2-phpstan-rules

ActivePhpstan-extension[Debugging &amp; Profiling](/categories/debugging)

mspirkov/yii2-phpstan-rules
===========================

A set of PHPStan rules for projects using the Yii2 framework

0.11.0(2w ago)19425—2.5%1[1 issues](https://github.com/mspirkov/yii2-phpstan-rules/issues)MITPHPPHP &gt;=7.4CI passing

Since Jul 4Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/mspirkov/yii2-phpstan-rules)[ Packagist](https://packagist.org/packages/mspirkov/yii2-phpstan-rules)[ RSS](/packages/mspirkov-yii2-phpstan-rules/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (8)Dependencies (44)Versions (16)Used By (0)

 [ ![](https://avatars0.githubusercontent.com/u/993323) ](https://github.com/yiisoft)

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. 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()` and component `behaviors()` 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.

[![PHP](https://camo.githubusercontent.com/de6657ab22d88fd2e8c1fda7dbffa5716c7d53ea6250959be771ea72d4863315/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f253345253344372e342d3741383642382e7376673f7374796c653d666f722d7468652d6261646765266c6f676f3d706870266c6f676f436f6c6f723d7768697465266c6162656c3d504850)](https://www.php.net/releases/7_4_0.php)[![Yii 2.0.x](https://camo.githubusercontent.com/a8b86019a796c90e714cf9d9b730c74f7c01dfef6f1704e928cb1d991fad0065/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f253345253344322e302e35332d3234374241302e7376673f7374796c653d666f722d7468652d6261646765266c6f676f3d796969266c6f676f436f6c6f723d7768697465266c6162656c3d596969)](https://github.com/yiisoft/yii2/tree/2.0.53)[![Tests](https://camo.githubusercontent.com/861e9ad6ca63c8f93a81489d90a5ffe0ddd241bca295c5b4fa2b05154b1122a7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d737069726b6f762f796969322d7068707374616e2d72756c65732f63692e796d6c3f6272616e63683d6d61696e267374796c653d666f722d7468652d6261646765266c6f676f3d676974687562266c6162656c3d5465737473)](https://github.com/mspirkov/yii2-phpstan-rules/actions/workflows/ci.yml)[![PHPStan](https://camo.githubusercontent.com/5d3f3dbee2905a1bc2d474f6b5932f627fd6986f6817fba7e172e7cea3ee6049/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d737069726b6f762f796969322d7068707374616e2d72756c65732f63692e796d6c3f6272616e63683d6d61696e267374796c653d666f722d7468652d6261646765266c6f676f3d676974687562266c6162656c3d5048505374616e)](https://github.com/mspirkov/yii2-phpstan-rules/actions/workflows/ci.yml)[![Coverage](https://camo.githubusercontent.com/041948576712080e99a1b5ef3b05ae7dfa0fb99f41266dc706d4e043c90bdeac/68747470733a2f2f696d672e736869656c64732e696f2f636f6465636f762f632f6769746875622f6d737069726b6f762f796969322d7068707374616e2d72756c65732e7376673f6272616e63683d6d61696e267374796c653d666f722d7468652d6261646765266c6f676f3d636f6465636f76266c6f676f436f6c6f723d7768697465266c6162656c3d436f766572616765)](https://codecov.io/github/mspirkov/yii2-phpstan-rules)[![PHPStan Level Max](https://camo.githubusercontent.com/347927812a31a0b0090fefb12765bc416adeb9383f8accf29740e8ffcdd43836/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d61782d3741383642382e7376673f7374796c653d666f722d7468652d6261646765266c6162656c3d5048505374616e2532304c6576656c)](https://camo.githubusercontent.com/347927812a31a0b0090fefb12765bc416adeb9383f8accf29740e8ffcdd43836/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d61782d3741383642382e7376673f7374796c653d666f722d7468652d6261646765266c6162656c3d5048505374616e2532304c6576656c)

What's inside
-------------

[](#whats-inside)

RuleCatches[`activeRecordRelationValidation`](#active-record-relations-validation)Invalid `hasOne()` / `hasMany()` link properties that do not exist on the current or related ActiveRecord model[`componentBehaviorsValidation`](#component-behaviors-validation)Malformed or invalid `behaviors()` in `yii\base\Component` — unknown behavior classes, bad config keys, and bad option types[`modelRulesValidation`](#model-validation-rules-validation)Malformed or invalid `rules()` in `yii\base\Model` — unknown validators, missing required options, bad regexes, unknown attributes, and more[`modelAttributeLabelsValidation`](#model-attribute-labels-validation)`attributeLabels()` entries in `yii\base\Model` that target attributes that don't exist, or use an empty attribute name[`noComplexControllerActions`](#complexity-limits)Controller actions with too much branching/looping — logic that belongs in a service[`noComplexActionClasses`](#complexity-limits)The same, for standalone `yii\base\Action` classes[`noControllerActionCallsViaThis`](#no-calling-actions-via-this)`$this->actionFoo()` inside a controller instead of a redirect or shared method[`noDbQueriesInControllers`](#no-database-access-outside-repositories)Direct DB/ActiveRecord access in controllers[`noDbQueriesInActions`](#no-database-access-outside-repositories)Direct DB/ActiveRecord access in `Action` classes[`noDbQueriesInViews`](#no-database-access-outside-repositories)Direct DB/ActiveRecord access in view files[`noDynamicQueryWhere`](#no-dynamic-sql-strings)String-concatenated conditions passed to `Query::where()` / `andWhere()`[`noForbiddenYiiAppProperties`](#taming-yiiapp)Reads of arbitrary `yii\base\Application` components, including `Yii::$app->*`[`noYiiAppPropertyMutation`](#taming-yiiapp)Writes to `yii\base\Application` properties, including `setComponents()`[`noDirectSuperglobals`](#no-raw-superglobals)Direct use of `$_GET`, `$_POST`, `$_SESSION`, etc.Every rule ships with its own PHPStan error identifier (`mspirkovYii2Rules.*`), so you can target `ignoreErrors` precisely instead of silencing a whole rule.

Installation
------------

[](#installation)

```
php composer.phar require --dev mspirkov/yii2-phpstan-rules
```

If your project uses [`phpstan/extension-installer`](https://github.com/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
-------------

[](#configuration)

All rules are on by default. Turn the whole set off, or tune individual rules, under `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
```

The rules
---------

[](#the-rules)

### Active Record relations validation

[](#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`.

```
/**
 * @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
{
}
```

### Component behaviors validation

[](#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: class strings, `class` / `__class` config arrays, direct `Behavior` instances, unknown classes, classes that do not extend `yii\base\Behavior`, unknown config options, and option value types inferred from public properties or setters.

```
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 validation rules validation

[](#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`:

```
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],                 // ✓
    ];
}
```

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.

```
/**
 * @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 attribute labels validation

[](#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:

```
/**
 * @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
        ];
    }
}
```

### Complexity limits

[](#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(): 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));
}
```

### No calling actions via `$this`

[](#no-calling-actions-via-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]);
}
```

### No database access outside repositories

[](#no-database-access-outside-repositories)

Fires on `ActiveRecord::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.

```
// ✗ flagged in a view: queries the database instead of just rendering data

// ✓ the controller/action fetches the data, the view only renders it

```

`noDbQueriesInControllers` / `noDbQueriesInActions` 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 dynamic SQL strings

[](#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`

[](#taming-yiiapp)

Two rules keep the application object from becoming a place where any property can be read or reassigned from anywhere. They check expressions typed as `yii\base\Application`, so the same restrictions apply to direct `Yii::$app` usage, variables holding it, and explicit `Application` instances:

```
// ✗ 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) {}
```

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

[](#no-raw-superglobals)

```
// ✗ 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');
```

Covers `$_GET`, `$_POST`, `$_REQUEST`, `$_SESSION`, `$_COOKIE`, `$_FILES`, and `$_SERVER`, each pointing at the matching `yii\web\Request` / `Session` / `UploadedFile` API.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance92

Actively maintained with recent releases

Popularity26

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity31

Early-stage or recently created project

 Bus Factor1

Top contributor holds 97.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

15

Last Release

19d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/45cacb752e41df997e47ec498acabbc548e07ae1c82d2d1a9a0f468c37e52a8e?d=identicon)[mspirkov](/maintainers/mspirkov)

---

Top Contributors

[![mspirkov](https://avatars.githubusercontent.com/u/63721828?v=4)](https://github.com/mspirkov "mspirkov (40 commits)")[![staabm](https://avatars.githubusercontent.com/u/120441?v=4)](https://github.com/staabm "staabm (1 commits)")

---

Tags

phpphp7php8phpstan-extensionstatic-analysisyiiyii2yii2-extensionPHPStanstatic analysiscode analysiscode analyseyii2yii

###  Code Quality

TestsPHPUnit

Static AnalysisRector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mspirkov-yii2-phpstan-rules/health.svg)

```
[![Health](https://phpackages.com/badges/mspirkov-yii2-phpstan-rules/health.svg)](https://phpackages.com/packages/mspirkov-yii2-phpstan-rules)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.5k60.6M10.6k](/packages/larastan-larastan)[craftcms/cms

Craft CMS

3.6k3.7M3.4k](/packages/craftcms-cms)[shipmonk/dead-code-detector

Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.

5014.2M111](/packages/shipmonk-dead-code-detector)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

67475.0M1.6k](/packages/phpstan-phpstan-doctrine)[szepeviktor/phpstan-wordpress

WordPress extensions for PHPStan

33011.2M1.8k](/packages/szepeviktor-phpstan-wordpress)[calebdw/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

16132.2k4](/packages/calebdw-larastan)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
