PHP code example of mspirkov / yii2-phpstan-rules

1. Go to this page and download the library: Download mspirkov/yii2-phpstan-rules library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

mspirkov / yii2-phpstan-rules example snippets


/**
 * @property string $email
 * @property-read string $fullName
 */
final class ContactModel extends Model
{
    public $name;

    public function getPhone(): string { /* ... */ }

    public function setPhone(string $phone): void { /* ... */ }
}

/** @var ContactModel $model */

$form = ActiveForm::begin();

echo $form->field($model, 'name');     // ✓ declared property
echo $form->field($model, 'email');    // ✓ declared via @property
echo $form->field($model, 'phone');    // ✓ has both getPhone() and setPhone()
echo $form->field($model, 'fullName'); // ✗ read-only — declared via @property-read, nothing to write the submitted value back to
echo $form->field($model, 'nickname'); // ✗ typo — "nickname" is not a property on ContactModel

ActiveForm::end();

/**
 * @property-read Address $address
 */
class Customer extends ActiveRecord
{
    /** @return ActiveQuery<Order> */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    public function getAddress()
    {
        return $this->hasOne(Address::class, ['id' => 'address_id']);
    }
}

class Order extends ActiveRecord
{
    /** @return ActiveQuery<Item> */
    public function getItems()
    {
        return $this->hasMany(Item::class, ['order_id' => 'id']);
    }
}

class Address extends ActiveRecord
{
    /** @return ActiveQuery<Country> */
    public function getCountry()
    {
        return $this->hasOne(Country::class, ['id' => 'country_id']);
    }
}

class Item extends ActiveRecord { /* ... */ }
class Country extends ActiveRecord  { /* ... */ }

Customer::find()->with('orders')->all();           // ✓
Customer::find()->with('orders.items')->all();     // ✓ Order declares its own "items" relation
Customer::find()->with('address.country')->all();  // ✓ related model resolved via @property-read
Customer::find()->joinWith('orders o')->all();     // ✓ alias is stripped before the relation is checked
Customer::find()->with('oders')->all();             // ✗ typo — no such relation on Customer
Customer::find()->with('orders.oops')->all();       // ✗ typo — no such relation on Order

/**
 * @property int $id
 * @property int $status
 * @property string $updated_at
 */
final class Customer extends ActiveRecord { /* ... */ }

Customer::findOne(1);                                         // ✓ primary key lookup, not a condition hash
Customer::findOne(['status' => 1]);                           // ✓
Customer::findOne(['status' => [1, 2]]);                      // ✓ IN (1, 2)
Customer::findOne(['updated_at' => new Expression('NOW()')]); // ✓ raw SQL, not type-checked
Customer::findOne(['statuss' => 1]);                          // ✗ typo — unknown attribute
Customer::findOne(['status' => '1']);                         // ✗ wrong type — int expected
Customer::deleteAll(['statuss' => 1]);                        // ✗ typo — unknown attribute
Customer::updateAll(['status' => 1], ['idd' => 5]);           // ✗ typo — unknown attribute in the condition

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

/**
 * @property int $id
 * @property int $status
 * @property int $age
 * @property string $updated_at
 */
final class Customer extends ActiveRecord { /* ... */ }

Customer::updateAll(['status' => 1], ['id' => 5]);              // ✓
Customer::updateAll(['updated_at' => new Expression('NOW()')]); // ✓ raw SQL, not type-checked
Customer::updateAll(['statuss' => 1]);                          // ✗ typo — unknown attribute
Customer::updateAll(['status' => 'active']);                    // ✗ wrong type — int expected
Customer::updateAllCounters(['age' => 1]);                      // ✓
Customer::updateAllCounters(['agee' => 1]);                     // ✗ typo — unknown attribute

$countQuery = Article::find()->where(['status' => 1]);

new Pagination(['totalCount' => $countQuery->count()]);  // ✓
new Pagination(['totalCoutn' => 100]);                   // ✗ typo — unknown option "totalCoutn"
new Pagination(['defaultPageSize' => '20']);             // ✗ wrong type — int expected

/**
 * @property string $title
 * @property string $slug
 * @property string $created_at
 */
final class Post extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            [
                'class' => TimestampBehavior::class,
                'createdAtAttribute' => 'created_at',   // ✓
                'updatedAtAttribute' => 'udpated_at',   // ✗ typo — unknown attribute
            ],
            [
                'class' => SluggableBehavior::class,
                'attribute' => 'titel',                 // ✗ typo — unknown attribute
                'slugAttribute' => 'slug',              // ✓
            ],
            [
                'class' => AttributeTypecastBehavior::class,
                'attributeTypes' => [
                    'created_at' => AttributeTypecastBehavior::TYPE_STRING, // ✓
                ],
            ],
        ];
    }
}

/**
 * @property string $created_at
 * @property string|null $updated_at
 */
final class Article extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            [
                'class' => TimestampBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], // ✓
                ],
            ],
            [
                'class' => BlameableBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_INSERT => ['created_by', 'updated_by'], // ✗ neither attribute exists
                ],
            ],
            [
                'class' => SluggableBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_VALIDATE => 'slug',     // ✗ unknown attribute
                ],
            ],
            [
                'class' => DateTimeBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_UPDATE => 'updated_at', // ✓
                ],
            ],
        ];
    }
}

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

public function actions(): array
{
    return [
        'error' => [
            'class' => ErrorAction::class,
            'vieww' => 'error',            // ✗ typo — unknown option
        ],
        'captcha' => [
            'class' => CaptchaAction::class,
            'fixedVerifyCode' => 1,        // ✗ string expected
        ],
        'invalid' => stdClass::class,      // ✗ not a yii\base\Action

        'download' => [
            'class' => DownloadAction::class,
            'path' => '@app/uploads',      // ✓
        ],
    ];
}

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;
}

/** @var ContactModel $model */

echo Html::activeLabel($model, 'name');    // ✓ declared property
echo Html::activeInput('text', $model, 'email');  // ✓ declared via @property
echo Html::activeTextInput($model, 'nema');       // ✗ typo — "nema" is not a property on ContactModel
echo Html::activeHint($model, 'nickname');        // ✗ typo — "nickname" is not a property on ContactModel

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;

    public function attributeHints(): array
    {
        return [
            'name' => 'Your full name',
            'emial' => 'We will reply here',   // ✗ typo — "emial" is not a property on ContactModel
            'email' => 'We will reply here',   // ✓ declared via @property
        ];
    }
}

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

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

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;

    public function rules(): array
    {
        return [
            ['name', '   }
}

final class ContactModel extends Model
{
    public $name;
    public $email;

    public function scenarios(): array
    {
        return [
            'create' => ['name', 'email'],
            'update' => ['name', '!emial'],  // ✗ typo — "emial" is not a property on ContactModel
            '' => ['name'],                  // ✗ empty scenario name
            'delete' => 'name',              // ✗ must be an array of attribute names
        ];
    }
}

$query->where(['in', 'status']);                                    // ✗ missing the values operand — needs 2
$query->andWhere(['between', 'age', 18]);                           // ✗ missing the upper bound — needs 3
$query->orWhere(['not', ['in', 'status']]);                         // ✗ same as above, nested inside "not"
$query->where(['>=', 'age', 18, 30]);                               // ✗ arbitrary operator, extra operand — needs exactly 2
$query->where(['and']);                                             // ✗ empty "and" — needs at least 1 operand

$query->where(['in', 'status', [1, 2]]);                            // ✓
$query->andWhere(['between', 'age', 18, 65]);                       // ✓
$query->orWhere(['and', ['status' => 1], ['in', 'type', [1, 2]]]);  // ✓

final class UploadForm extends Model
{
    public $imageFile;
    public $imageFiles;

    public function rules(): array
    {
        return [[['imageFile', 'imageFiles'], 'file']];
    }
}

/** @var UploadForm $model */

$model->imageFile = UploadedFile::getInstance($model, 'imageFile');    // ✓
$model->imageFiles = UploadedFile::getInstances($model, 'imageFiles'); // ✓
$model->imageFile = UploadedFile::getInstance($model, 'imagefile');    // ✗ typo — "imagefile" is not a property on UploadForm
$files = UploadedFile::getInstances($model, 'imagefiles');             // ✗ typo — "imagefiles" is not a property on UploadForm

ActiveForm::begin([
    'method' => 'get',             // ✓ declared property
    'metod' => 'get',              // ✗ typo — unknown option "metod"
    'encodeErrorSummary' => 'yes', // ✗ wrong type — bool expected, string given
]);

ActiveForm::end();

Yii::createObject([
    'traceLevel' => 3,        // ✗ missing "class" or "__class"
]);

Yii::createObject([
    'class' => Logger::class,
    'traceLevel' => '3',      // ✗ wrong type — int expected
    'flushInteval' => 1000,   // ✗ typo — unknown option (should be "flushInterval")
]);

Yii::createObject([
    'class' => Logger::class,
    'traceLevel' => 3,        // ✓
]);

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

// ✗ 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]);
}

// ✗ flagged in a view: queries the database instead of just rendering data
 foreach (Post::find()->where(['status' => 1])->all() as $post): 

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

// ✗ flagged: string-built condition, one step from SQL injection
$query->where("status = $status");
$query->andWhere('status = ' . $status);
$query->orWhere("id IN ($ids)");

// ✓ array condition syntax — parameterized, and PHPStan can see the shape
$query->where(['status' => $status]);
$query->andWhere(['in', 'id', $ids]);

// ✗ arbitrary component access
$cache = Yii::$app->cache;

// ✓ inject the component instead
public function __construct(private CacheInterface $cache) {}

// ✗ flagged: fetches a full row just to test whether one is there
public function emailIsTaken(string $email): bool
{
    return User::find()->where(['email' => $email])->one() !== null;
}

// ✗ flagged: counts every matching row just to test whether one is there
public function emailIsAvailable(string $email): bool
{
    return User::find()->where(['email' => $email])->count() < 1;
}

// ✓ exists() only asks the database whether a row is there
public function emailIsTaken(string $email): bool
{
    return User::find()->where(['email' => $email])->exists();
}

// ✓ same, negated
public function emailIsAvailable(string $email): bool
{
    return !User::find()->where(['email' => $email])->exists();
}

/**
 * @var numeric-string $id
 * @var string $name
 */

echo Html::encode($id);   // ✗ flagged — $id can only ever be a numeric-string
echo Html::encode($name); // ✓ a plain string may still contain special characters

// ✗ mutation of properties
Yii::$app->params = [...];
Yii::$app->setComponents([...]);