mspirkov/yii2-rector

A set of Rector rules for projects using the Yii2 framework

Maintainers

Package info

github.com/mspirkov/yii2-rector

Type:rector-extension

pkg:composer/mspirkov/yii2-rector

Transparency log

Statistics

Installs: 290

Dependents: 2

Suggesters: 1

Stars: 1

Open Issues: 0

0.1.1 2026-09-01 12:59 UTC

This package is auto-updated.

Last update: 2026-09-01 15:30:40 UTC


README

Yii2 Rector

A set of Rector rules for Yii2 projects that I put together for my own day-to-day work. They make refactoring a Yii2 codebase easier and help keep it cleaner, automating the framework-specific patterns — magic properties, ActiveRecord/Query calls, accumulated deprecations — that a generic Rector set has no way to know about.

PHP Yii2 Rector Tests Coverage PHPStan Level Max

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 Rector. The more up‑to‑date the versions, the better the refactoring.

composer require --dev mspirkov/yii2-rector

Usage

use MSpirkov\Yii2\Rector\Yii2SetList;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withSets([
        Yii2SetList::MAIN,
    ]);

Enabling individual rules

Individual rules can be enabled on their own via ->withRules([...]) instead of ->withSets([...]):

use MSpirkov\Yii2\Rector\Rules\ReplaceClassnameWithClassRector;
use MSpirkov\Yii2\Rector\Rules\ReplaceExistenceCheckWithExistsRector;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withRules([
        ReplaceClassnameWithClassRector::class,
        ReplaceExistenceCheckWithExistsRector::class,
    ]);

Skipping rules

Any rule — whether pulled in through Yii2SetList::MAIN or added individually — can be turned off entirely via ->withSkip([...]):

use MSpirkov\Yii2\Rector\Rules\MergeModelRulesRector;
use MSpirkov\Yii2\Rector\Yii2SetList;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withSets([
        Yii2SetList::MAIN,
    ])
    ->withSkip([
        MergeModelRulesRector::class,
    ]);

Mapping a rule to a list of paths/patterns instead skips it only there, leaving it active everywhere else — handy for legacy code that isn't ready for a particular rule yet:

    ->withSkip([
        MergeModelRulesRector::class => [
            __DIR__ . '/src/Legacy/*',
        ],
    ]);

A plain path/pattern (no rule class key) skips those files from every rule, Yii2-specific or not.

Configuring a rule

AddPropertyTagsRector and RemoveRedundantPropertyTagsRector accept a skippedClasses option — see the rule reference below for the exact shape of each — and AddPropertyTagsRector additionally accepts insertBeforeTags. Configure them via ->withConfiguredRule(), using the rule's own constants as keys:

use MSpirkov\Yii2\Rector\Rules\AddPropertyTagsRector;
use MSpirkov\Yii2\Rector\Yii2SetList;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withSets([
        Yii2SetList::MAIN,
    ])
    ->withConfiguredRule(AddPropertyTagsRector::class, [
        'skippedClasses' => [
            'App\Models\LegacyModel',
            'App\Models\Product' => ['internalNotes'],
        ],
        'insertBeforeTags' => ['@author', '@since'],
    ]);

App\Models\LegacyModel above is skipped entirely (a plain array value), while only the internalNotes property is skipped on App\Models\Product (a class-name key mapped to a list of property names) — every other property on it is still processed normally.

Rules at a glance

Rule Description
AddPropertyTagsRector Add (or correct) @property/@property-read/@property-write tags on a yii\base\BaseObject subclass, based on its own getXxx()/setXxx() method pairs and ActiveRecord relation getters (hasOne()/hasMany()).
MergeModelRulesRector Merge yii\base\Model::rules() entries that configure the same validator with the same options but a different attribute into one entry, combining their attributes into a single array (an attribute already present in another merged entry is not duplicated).
RemoveRedundantHtmlEncodeRector Remove a yii\helpers\Html::encode() call whose $content argument PHPStan proves is a numeric string — digits only can't contain a character htmlspecialchars() would touch, so the call is replaced by its bare $content argument (dropping a trailing $doubleEncode argument, if present, too).
RemoveRedundantPropertyTagsRector Remove a @property/@property-read/@property-write tag from a yii\base\BaseObject subclass when neither a matching public getXxx() nor setXxx() method exists (own or inherited) — typically left behind after the accessor it documented was renamed or removed.
ReplaceClassnameWithClassRector Replace the deprecated yii\base\BaseObject::className() call with the native ::class constant.
ReplaceExistenceCheckWithExistsRector Replace an existence check on a yii\db\QueryInterface result with the cheaper ->exists() call.
ReplaceFindWhereAllWithFindAllRector Replace find()->where([...])->all() on an ActiveRecord class with the equivalent findAll([...]).
ReplaceFindWhereOneWithFindOneRector Replace find()->where([...])->one() on an ActiveRecord class with the equivalent findOne([...]).
ReplaceGetterWithPropertyRector Replace a yii\base\BaseObject getter call with the equivalent magic-property access, when the property is documented via a class-level @property or @property-read tag whose type matches the getter's return type, and there is no public native property of the same name (which would bypass the getter entirely)
ReplaceSetterWithPropertyRector Replace a yii\base\BaseObject setter call with the equivalent magic-property assignment, when the property is documented via a class-level @property or @property-write tag whose type matches the setter's parameter type, and there is no public native property of the same name (which would bypass the setter entirely)
ReplaceWhereEqualityConditionWithArrayRector Replace a single-column string where()/andWhere()/orWhere() condition (interpolated or concatenated) with the safer array condition format

Rule reference

AddPropertyTagsRector

Add (or correct) @property/@property-read/@property-write tags on a yii\base\BaseObject subclass, based on its own getXxx()/setXxx() method pairs and ActiveRecord relation getters (hasOne()/hasMany()). A class whose __get()/__set() is overridden by something other than yii\base\BaseObject, yii\base\Component, yii\db\BaseActiveRecord, or yii\base\DynamicModel is skipped entirely, since such magic properties may not correspond to getXxx()/setXxx() methods. Configurable via skippedClasses — a plain array value (e.g. 'App\Foo') fully skips a class, while a string key mapped to a list of property names (e.g. 'App\Bar' => ['name']) skips only those properties — and insertBeforeTags, a list of PHPDoc tag names (defaulting to ['@author', '@since', '@mixin']) before which newly added @property* tags are inserted

+/**
+ * @property string $name The product name.
+ * @property-read int $price
+ * @property-write float $discount
+ */
 class Product extends BaseObject
 {
     private string $_name;

     private int $_price;

     private float $_discount;

     /**
      * @return string The product name.
      */
     public function getName(): string
     {
         return $this->_name;
     }

     /**
      * @param string $name The product name.
      */
     public function setName(string $name): void
     {
         $this->_name = $name;
     }

     public function getPrice(): int
     {
         return $this->_price;
     }

     public function setDiscount(float $discount): void
     {
         $this->_discount = $discount;
     }
 }
+/**
+ * @property-read Customer|null $customer
+ * @property-read OrderItem[] $items
+ */
 class Order extends ActiveRecord
 {
     public function getCustomer(): ActiveQuery
     {
         return $this->hasOne(Customer::class, ['id' => 'customer_id']);
     }

     public function getItems(): ActiveQuery
     {
         return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
     }
 }

MergeModelRulesRector

Merge yii\base\Model::rules() entries that configure the same validator with the same options but a different attribute into one entry, combining their attributes into a single array (an attribute already present in another merged entry is not duplicated). Two entries only merge when everything after the attribute(s) — the validator and any options — is identical; a rules() body that isn't a single return [...] of literal rule arrays is left untouched

 class LoginForm extends Model
 {
     public function rules(): array
     {
         return [
-            ['login', 'required'],
-            ['password', 'required'],
+            [['login', 'password'], 'required'],
         ];
     }
 }

RemoveRedundantHtmlEncodeRector

Remove a yii\helpers\Html::encode() call whose $content argument PHPStan proves is a numeric string — digits only can't contain a character htmlspecialchars() would touch, so the call is replaced by its bare $content argument (dropping a trailing $doubleEncode argument, if present, too). Any other $content is left untouched

 <?php
 /**
  * @var numeric-string $id
  * @var string $name
  */
 ?>
-<?= Html::encode($id) ?>
+<?= $id ?>
 <?= Html::encode($name) ?>

RemoveRedundantPropertyTagsRector

Remove a @property/@property-read/@property-write tag from a yii\base\BaseObject subclass when neither a matching public getXxx() nor setXxx() method exists (own or inherited) — typically left behind after the accessor it documented was renamed or removed. A tag backed by at least one accessor is left untouched even if it names the wrong direction (e.g. @property-read with only a setter) — correcting it to match the accessor that does exist is AddPropertyTagsRector's job, not this rule's, so the two never touch the same tag. A class whose __get()/__set() isn't the one inherited from yii\base\BaseObject or yii\base\Component — own override or inherited from some other ancestor, including yii\db\BaseActiveRecord and yii\base\DynamicModel — is skipped entirely, since its magic properties aren't necessarily backed by getter/setter methods. Configurable via skippedClasses — a plain array value (e.g. 'App\Foo') fully skips a class, while a string key mapped to a list of property names (e.g. 'App\Bar' => ['name']) skips only those properties

 /**
  * @property string $name
- * @property-read int $legacyCount
  */
 class Product extends BaseObject
 {
     private string $_name;

     public function getName(): string
     {
         return $this->_name;
     }

     public function setName(string $name): void
     {
         $this->_name = $name;
     }
 }

ReplaceClassnameWithClassRector

Replace the deprecated yii\base\BaseObject::className() call with the native ::class constant. self::className() and parent::className() are left untouched, since both are late-static-binding forwarding calls not generally equivalent to self::class/parent::class once the class is subclassed — only static::className() and an explicit class name are rewritten

-$class = SomeClass::className();
-$class = static::className();
+$class = SomeClass::class;
+$class = static::class;

ReplaceExistenceCheckWithExistsRector

Replace an existence check on a yii\db\QueryInterface result with the cheaper ->exists() call. Recognises a ->count() comparison against the boundary literals 0/1 (in either operand order) and a strict ->one() !== null / ->one() === null check. A check that means "no rows" (e.g. count() < 1, one() === null) is rewritten to the negated !exists(), not exists(). Only the boundary comparisons that map unambiguously onto a presence/absence question are recognised — count() > 1, for instance, is left untouched

 public function emailIsTaken(string $email): bool
 {
-    return User::find()->where(['email' => $email])->one() !== null;
+    return User::find()->where(['email' => $email])->exists();
 }

 public function emailIsAvailable(string $email): bool
 {
-    return User::find()->where(['email' => $email])->count() < 1;
+    return !User::find()->where(['email' => $email])->exists();
 }

ReplaceFindWhereAllWithFindAllRector

Replace find()->where([...])->all() on an ActiveRecord class with the equivalent findAll([...]). Only fires when the where() condition is a literal array keyed entirely by string literals: findAll() treats any other condition shape (scalar, list, Expression) as a primary key lookup instead of forwarding it to where() unchanged, so those shapes are intentionally left untouched.

-$customers = Customer::find()->where(['status' => 1])->all();
+$customers = Customer::findAll(['status' => 1]);

ReplaceFindWhereOneWithFindOneRector

Replace find()->where([...])->one() on an ActiveRecord class with the equivalent findOne([...]). Only fires when the where() condition is a literal array keyed entirely by string literals: findOne() treats any other condition shape (scalar, list, Expression) as a primary key lookup instead of forwarding it to where() unchanged, so those shapes are intentionally left untouched.

-$customer = Customer::find()->where(['status' => 1])->one();
+$customer = Customer::findOne(['status' => 1]);

ReplaceGetterWithPropertyRector

Replace a yii\base\BaseObject getter call with the equivalent magic-property access, when the property is documented via a class-level @property or @property-read tag whose type matches the getter's return type, and there is no public native property of the same name (which would bypass the getter entirely)

 /**
  * @property-read string $prop
  */
 class Example extends BaseObject
 {
     private string $_prop;

     public function getProp(): string
     {
         return $this->_prop;
     }
 }

-$value = (new Example())->getProp();
+$value = (new Example())->prop;

ReplaceSetterWithPropertyRector

Replace a yii\base\BaseObject setter call with the equivalent magic-property assignment, when the property is documented via a class-level @property or @property-write tag whose type matches the setter's parameter type, and there is no public native property of the same name (which would bypass the setter entirely)

 /**
  * @property-write string $prop
  */
 class Example extends \yii\base\BaseObject
 {
     private string $_prop;

     public function setProp(string $value): void
     {
         $this->_prop = $value;
     }
 }

-(new Example())->setProp('value');
+(new Example())->prop = 'value';

ReplaceWhereEqualityConditionWithArrayRector

Replace a single-column string where()/andWhere()/orWhere() condition (interpolated or concatenated) with the safer array condition format

-$query->where("column = $value");
-$query->andWhere('column = ' . $value);
+$query->where(['column' => $value]);
+$query->andWhere(['column' => $value]);