Skip to content
yii2-extensionsPublic

About

Enhanced static analysis for Yii2 applications with PHPStan. Precise type inference, dynamic method resolution, and comprehensive property reflection.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

21 stars

Watchers

1 watching

Forks

Latest commit

 

History

127 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Yii Framework

PHPStan


PHPUnit PHPStan Security

Enhanced static analysis for Yii2 applications with PHPStan
Precise type inference, dynamic method resolution, and comprehensive property reflection

Features

Feature Overview

Installation

composer require --dev yii2-extensions/phpstan:^0.5

Upgrading from 0.4.x? See Upgrading to 0.5.0 for the inferred type changes.

Quick start

Create a phpstan.neon file in your project root.

includes:
    - vendor/yii2-extensions/phpstan/extension.neon

parameters:
    level: 5

    paths:
        - src
        - controllers
        - models

    tmpDir: %currentWorkingDirectory%/runtime

    yii2:
        config_path: config/phpstan-config.php
        component_generics:
            user: identityClass      # Built-in (already configured)
            repository: modelClass   # Custom generic component

Create a PHPStan-specific config file (config/phpstan-config.php).

<?php

declare(strict_types=1);

return [
    // Application params: array shape inferred for Yii::$app->params and module params
    'params' => [
        'turnstile.siteKey' => '',
        'adminEmail' => 'admin@example.com',
        'maxItems' => 100,
    ],
    // PHPStan only: used by this extension for behavior property/method type inference
    'behaviors' => [
        app\models\User::class => [
            app\behaviors\SoftDeleteBehavior::class,
            yii\behaviors\TimestampBehavior::class,
        ],
    ],
    'components' => [
        'db' => [
            'class' => yii\db\Connection::class,
            'dsn' => 'sqlite::memory:',
        ],
        'user' => [
            'class' => yii\web\User::class,
            'identityClass' => app\models\User::class,
        ],
        // Add your custom components here
    ],
];

Run PHPStan.

vendor/bin/phpstan analyse

Type inference examples

Active Record

Yii ships generic PHPDoc for ActiveRecord and ActiveQuery, so find(), findOne(), findAll(), hasOne(), hasMany(), one(), and all() are typed by Yii itself. This extension adds the asArray() row shapes built from the model @property tags and the getAttribute() return types. Tags inherited from parent classes, used traits, and interfaces are included, so a model that extends a generated base class gets the tags of that class. In a row, relation tags and read-only tags give optional keys, so guard them with ?? or isset().

A custom query class without generic PHPDoc, as Gii generates it, needs no changes: the extension binds find() to the model named at the call site, so one(), all(), batch(), and each() yield the model. A query reached another way, such as a typed parameter, takes its model from the one() and all() overrides that Gii generates.

// ✅ Typed as User|null
$user = User::findOne(1);

// ✅ Typed as array<User>
$users = User::findAll(['status' => 'active']);

// ✅ Generic ActiveQuery<User> preserved through method chaining
$query = User::find()->where(['active' => 1])->orderBy('name');

// ✅ Array results typed as array<array{id: int, name: string}>
$userData = User::find()->asArray()->all();

// ✅ Typed as Invoice|null, with InvoiceQuery generated by Gii
$invoice = Invoice::find()->where(['id' => 1])->one();

// ✅ Typed based on model property annotations string
$userName = $user->getAttribute('name');

// ✅ Behavior property resolution string
$slug = $user->getAttribute('slug');

Application components

Yii::$app is typed as the class set in phpstan.application_type (yii\web\Application by default).

// ✅ Typed based on your configuration
$mailer = Yii::$app->mailer; // MailerInterface
$db = Yii::$app->db;         // Connection
$user = Yii::$app->user;     // User

// ✅ User identity with proper type inference
if (Yii::$app->user->isGuest === false) {
    $userId = Yii::$app->user->id;           // int|string|null
    $identity = Yii::$app->user->identity;   // YourUserClass
}

A component is typed from a class name, an array with class or __class, a closure with a class return type, or an object, and a yii\di\Instance reference is typed as yii\di\Instance, which is what Yii returns for it. When its class can't be determined, as for a closure without a return type, Yii::$app->get('id') is typed as object, and so is Yii::$app->id, even when the application class declares it. A core component configured without a class, such as request with only cookieValidationKey, keeps the type the application class declares, since Yii completes its class. Add a class return type to the closure for the precise type.

Application params

// Types are inferred from the values in your phpstan-config.php 'params' key

// ✅ Typed as array{'turnstile.siteKey': string, adminEmail: string, maxItems: int}
$params = Yii::$app->params;

// ✅ Typed as string
$email = Yii::$app->params['adminEmail'];

// ✅ Typed as int
$maxItems = Yii::$app->params['maxItems'];

// ✅ Nested arrays are also supported
// 'nested' => ['db' => ['host' => 'localhost', 'port' => 3306]]
$host = Yii::$app->params['nested']['db']['host']; // string

// ✅ Module params share the configured shape, for example inside a controller
$maxItems = $this->module->params['maxItems']; // int

Keys that aren't valid identifiers, including keys with quotes, backslashes, whitespace, or line breaks, are part of the shape too. With no params configured, Yii::$app->params is typed as array.

Behaviors

// Behaviors are attached via the `phpstan-config.php` behaviors map (PHPStan only)

/**
 * @property string $slug
 * @property-read int $created_at
 *
 * Note: `created_at` is provided by `TimestampBehavior`.
 */
class SoftDeleteBehavior extends \yii\base\Behavior
{
    public function softDelete(): bool { /* ... */ }
}

// ✅ Typed based on your configuration
$user = new User();

// ✅ Typed as string (inferred from behavior property)
$slug = $user->getAttribute('slug');

// ✅ Direct property access is also inferred (behavior property)
$slug2 = $user->slug;

// ✅ Typed as int (inferred from behavior property)
$createdAt = $user->getAttribute('created_at');

// ✅ Typed as bool (method defined in attached behavior)
$result = $user->softDelete();

Dependency injection

$container = new Container();

// ✅ Type-safe service resolution
$service = $container->get(MyService::class); // MyService
$logger = $container->get('logger');          // LoggerInterface (if configured) or object

Container definitions accept the same forms as components, and an object with an __invoke() method is resolved from that method's return type. A definition whose class can't be determined leaves get() typed as object.

Header collection

$headers = new HeaderCollection();

// ✅ Typed as string|null
$host = $headers->get('Host');

// ✅ Typed as array<string>
$forwardedFor = $headers->get('X-Forwarded-For', ['127.0.0.1'], false);

// ✅ A non-nullable default removes null from the return type
$default = 'default-value';
$requestId = $headers->get('X-Request-ID', $default, true); // string
$allRequestIds = $headers->get('X-Request-ID', [$default], false); // array<string>

Logger messages

The logger and target message buffers are inferred directly without requiring stubs or local PHPDoc overrides.

use yii\log\Logger;

$logger = new Logger();

foreach ($logger->messages as $message) {
    $payload = $message[0];        // string|array<mixed>|Throwable|PsrMessage on Yii 22.0
    $level = $message[1];          // int
    $category = $message[2];       // string
    $timestamp = $message[3];      // float
    $trace = $message[4];          // list<array{file: string, line: int, ...}>
    $memory = $message[5] ?? null; // int|null
}

Each message tuple is a list, and the same tuple is inferred for yii\log\Target::$messages. Target::filterMessages() preserves the input key and tuple types while correctly allowing an empty filtered result, and an input known to be empty, such as [], stays array{}.

Service locator

$serviceLocator = new ServiceLocator();

// ✅ Get a component by its ID
$mailer = $serviceLocator->get('mailer');  // Mailer (if configured) or object

// A class name that no component uses keeps Yii's type, since get() throws for it at runtime
$service = $serviceLocator->get(Mailer::class);  // object

// Nullable when `$throwException` is not `true`
$user = $serviceLocator->get('user', false); // User|null

// ✅ User component with proper type inference in Action or Controller
$user = $this->controller->module->get('user'); // User

Documentation

For detailed configuration options and advanced usage.

Package information

PHP Yii 2.0.x Yii 22.0.x Latest Stable Version Total Downloads

Project status

Codecov PHPStan Level Max Quality StyleCI

Our social networks

Follow on X Follow on Facebook Join our Subreddit Join on Telegram

License

License

About

Enhanced static analysis for Yii2 applications with PHPStan. Precise type inference, dynamic method resolution, and comprehensive property reflection.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

21 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages