Search by

mspirkov / yii2-rector

mspirkov

A set of Rector rules for projects using the Yii2 framework

Package info

github.com/mspirkov/yii2-rector

Type:rector-extension

pkg:composer/mspirkov/yii2-rector

Statistics

Installs: 22 299

Dependents: 24

Suggesters: 1

Stars: 3

Open Issues: 0

0.2.0 2026-09-28 16:40 UTC

This package is auto-updated.

Last update: 2026-09-28 17:58:07 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 Total Downloads Tests Coverage PHPStan Level Max

Support

If you like this project, give it a ⭐ on GitHub — it helps others discover it.

Supported extensions

The ActiveRecord/Query rules aren't limited to the SQL-based yii\db\ActiveRecord — they also work with ActiveRecord/ActiveQuery classes from popular Yii2 storage extensions such as yiisoft/yii2-mongodb and yiisoft/yii2-redis.

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::DEPRECATION,
        Yii2SetList::CODE_QUALITY,
        Yii2SetList::PHPDOC,
    ]);

Sets

Every rule belongs to exactly one set, so you can enable only the kinds of changes you want:

Set Description
Yii2SetList::DEPRECATION Replaces deprecated Yii2 APIs with their current equivalents
Yii2SetList::CODE_QUALITY Removes redundant code and simplifies or optimizes framework-specific patterns
Yii2SetList::PHPDOC Generates and cleans up PHPDoc annotations, such as @property tags

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 a set 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::DEPRECATION,
        Yii2SetList::CODE_QUALITY,
        Yii2SetList::PHPDOC,
    ])
    ->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::DEPRECATION,
        Yii2SetList::CODE_QUALITY,
        Yii2SetList::PHPDOC,
    ])
    ->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.
ReplaceActionReturnLiteralWithExitCodeRector Replace a literal return 0;/return 1; in one of a console controller's action*() methods with the equivalent ExitCode::OK/ExitCode::UNSPECIFIED_ERROR constant.
ReplaceAppRequestResponseWithThisRector Replace Yii::$app->request/Yii::$app->response with $this->request/$this->response inside a yii\base\Controller subclass — the controller already exposes the same request/response objects through its own properties
ReplaceCacheMultiMethodAliasesRector Replace a deprecated yii\caching\Cache multi-key method alias — mget(), mset(), or madd() — with its canonical multiGet(), multiSet(), or multiAdd() equivalent
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.
ReplaceExitCodeConstantRector Replace a console controller's deprecated Controller::EXIT_CODE_NORMAL/EXIT_CODE_ERROR constant with the ExitCode::OK/ExitCode::UNSPECIFIED_ERROR equivalent
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([...]).
ReplaceGetHasChangedWithIsChangedRector Replace the deprecated yii\caching\Dependency::getHasChanged() call with isChanged()
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)
ReplaceTraceWithDebugRector Replace the deprecated Yii::trace() call with Yii::debug()
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;
     }
 }

ReplaceActionReturnLiteralWithExitCodeRector

Replace a literal return 0;/return 1; in one of a console controller's action*() methods with the equivalent ExitCode::OK/ExitCode::UNSPECIFIED_ERROR constant. A return inside a closure/arrow function nested in the action method is left untouched, since it isn't the action's own exit code

 class ProcessController extends Controller
 {
     public function actionRun(bool $allowed)
     {
         if (!$allowed) {
-            return 1;
+            return ExitCode::UNSPECIFIED_ERROR;
         }

-        return 0;
+        return ExitCode::OK;
     }
 }

ReplaceAppRequestResponseWithThisRector

Replace Yii::$app->request/Yii::$app->response with $this->request/$this->response inside a yii\base\Controller subclass — the controller already exposes the same request/response objects through its own properties

 class SiteController extends Controller
 {
     public function actionIndex()
     {
-        Yii::$app->response->format = Response::FORMAT_JSON;
+        $this->response->format = Response::FORMAT_JSON;

-        $ip = Yii::$app->request->getUserIP();
+        $ip = $this->request->getUserIP();
     }
 }

ReplaceCacheMultiMethodAliasesRector

Replace a deprecated yii\caching\Cache multi-key method alias — mget(), mset(), or madd() — with its canonical multiGet(), multiSet(), or multiAdd() equivalent

-$cache->mget(['key1', 'key2']);
-$cache->mset(['key1' => 'value1', 'key2' => 'value2']);
-$cache->madd(['key3' => 'value3']);
+$cache->multiGet(['key1', 'key2']);
+$cache->multiSet(['key1' => 'value1', 'key2' => 'value2']);
+$cache->multiAdd(['key3' => 'value3']);

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();
 }

ReplaceExitCodeConstantRector

Replace a console controller's deprecated Controller::EXIT_CODE_NORMAL/EXIT_CODE_ERROR constant with the ExitCode::OK/ExitCode::UNSPECIFIED_ERROR equivalent

 class ProcessController extends Controller
 {
     public function actionRun()
     {
-        return self::EXIT_CODE_NORMAL;
+        return ExitCode::OK;
     }
 }

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]);

ReplaceGetHasChangedWithIsChangedRector

Replace the deprecated yii\caching\Dependency::getHasChanged() call with isChanged()

-$hasChanged = $dependency->getHasChanged($cache);
+$hasChanged = $dependency->isChanged($cache);

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';

ReplaceTraceWithDebugRector

Replace the deprecated Yii::trace() call with Yii::debug()

-Yii::trace('Some message');
-Yii::trace($data, __METHOD__);
+Yii::debug('Some message');
+Yii::debug($data, __METHOD__);

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]);