# Migration guide

> PHP 8.3 or later is now required. Update the PHP runtime used for analysis, CI and development containers.

## PHP 8.3 requirement

PHP 8.3 or later is now required. Update the PHP runtime used for analysis, CI and development containers.
If the application sets Composer `config.platform.php`, update it to at least 8.3 as well.

## Upgrading to 0.5.0

For Yii 2.0 applications, update the framework constraint to `~2.0.55` before upgrading this extension. Yii 22
applications retain their framework requirement. See [requirements](/docs/extensions/phpstan/installation/index.md#requirements).

Yii ships generic and conditional PHPDoc types
for `ActiveRecord`, `ActiveQuery`, `Container`, `ServiceLocator`, and `HeaderCollection`, so the extension no longer
duplicates them: `find()`, `findOne()`, `findAll()`, `findBySql()`, `hasOne()`, `hasMany()`, `one()`, and `all()` are
typed by Yii itself. The extension adds what that PHPDoc can't express: the model of a custom query class without
generic PHPDoc, such as one generated by Gii, taken from the `find()` call site or from the class's own `one()` and
`all()` overrides, `asArray()` row shapes built from the model `@property` tags, `getAttribute()` typing, and the
application class, params, components, behaviors, and container services from your configuration file.

Your configuration doesn't change: `yii2.config_path`, `phpstan.application_type`, and `params` work as before, and so do
narrowing, assignments, and write checks on `Yii::$app` and its params. The extension no longer generates stub files at
runtime. It ships a static stub, `stubs/yii.stub`, registered through `stubFiles` in `extension.neon`, that declares
`yii\BaseYii::$app` and `yii\base\Module::$params` with placeholder PHPDoc types, and a PHPDoc type resolver of the
extension (`ApplicationTypeNodeResolverExtension`) turns them into the configured application class and params shape. A
writable directory for generated stubs is no longer needed (PHPStan still needs a writable result cache), and the `yii2-phpstan-stub-*.stub` files that 0.4.x wrote to the
system temporary directory and never removed can be deleted.

If `extension.neon` is included from a path outside your project's Composer vendor directory, such as a checkout of this
repository analyzing itself, a git submodule, or a monorepo or path repository included by its real path, PHPStan
validates `stubs/yii.stub` as a project stub. A Composer install included through `vendor/`, symlinked or not, isn't
affected. The validation runs only on a full analysis with a cold result cache, and its errors can't be suppressed with
`ignoreErrors`. It can report two errors:

- `class.notFound` for the configured application class, since the stub is validated against the classes declared in
  stub files only. Declare that class in a stub file of your project, for the default
  `<?php namespace yii\web { class Application {} }`, and add that file to `stubFiles`; this repository does so with
  `tests/support/application-classes.stub`, registered in `phpstan.neon`.
- `missingType.iterableValue` at level 6 or higher when no `params` are configured. Configure at least one param, or
  include the extension through `vendor/`.

### Inferred type changes

Update `@var` and `@return` annotations, and `assertType()` expectations, that rely on the previous types.

| Call                                                              | Before (0.4.x)                      | After (0.5.0)                            |
| ----------------------------------------------------------------- | ----------------------------------- | ---------------------------------------- |
| `Model::find()->all()`, `$model->hasMany(...)->all()`             | `array<int, Model>`                 | `array<Model>`                           |
| `Model::find()->asArray()->all()`                                 | `array<int, array{...}>`            | `array<array{...}>`                      |
| `static::find()`, `self::find()` inside a model                   | `ActiveQuery<Model>`                | `ActiveQuery<static(Model)>`             |
| `$model->hasOne($flag ? A::class : B::class, [...])`              | `ActiveQuery` of one class only     | `ActiveQuery<A\|B>`                      |
| `$model->hasOne($class, [...])` with `class-string<User> $class`  | PHPStan internal error              | `ActiveQuery<User>`                      |
| `CommentQuery` method chain, then `one()`                         | `mixed`                             | `Comment\|null`                          |
| `Article::find()->asArray()`                                      | `ActiveQuery<array<string, mixed>>` | `ActiveQuery<array{id: int, ...}>`       |
| `asArray()` row key of a `@property Comment[] $comments` tag      | `comments: array<Comment>`          | `comments?: array<array<string, mixed>>` |
| `asArray()` row key of a `@property Category\|null $category` tag | `category: Category\|null`          | `category?: array<string, mixed>\|null`  |
| `asArray()` row key of a `@property-read string $label` tag       | `label: string`                     | `label?: string`                         |
| `$article->getAttribute('id')`                                    | `mixed`                             | `int`                                    |
| `$container->get('unknown')`, `$container->get($id)`              | `mixed`                             | `object`                                 |
| `Yii::$app->get('unknown')`, `$locator->get($id)`                 | `mixed`                             | `object`                                 |
| `Yii::$app->get(User::class)`, `$locator->get(Foo::class)`        | `User`, `Foo`                       | `object`                                 |
| `$locator->get('user', false)`, `$locator->get('user', $bool)`    | `User`                              | `User\|null`                             |
| `$headers->get($name, null, false)`                               | `array<int, string>\|null`          | `array<string>\|null`                    |
| `$headers->get($name, [], false)`                                 | `array<int, string>`                | `array<string>`                          |
| `Target::filterMessages([])`                                      | array of `never`                    | `array{}`                                |

`CommentQuery` stands for a custom query class declared with `@extends ActiveQuery<Comment>`, and `Article` for a model
without `@property` tags of its own that extends a base class declaring them. A model whose `find()` returns a custom
query class without generic PHPDoc gets `static::find()` and `self::find()` typed as
`ModelQuery&yii\db\ActiveQuery<static(Model)>` instead. Relation calls with `static::class` or `get_class($this)` no
longer abort the analysis either; `$this->hasMany(static::class, [...])` is `ActiveQuery<static(Model)>`.
`Yii::$app->get()`, `$module->get()`, and any other service locator resolve component IDs only: a class name or a
container service ID that no component uses keeps the type Yii declares, `object`, since the service locator throws
for it at runtime. Use `Yii::$container->get(Foo::class)` for container services.

Relation keys, which an `asArray()` row holds only when the relation is eager-loaded with `with()`, and read-only keys
are optional. Reading one directly, as in `$row['comments']`, is reported at level 7 and higher as an offset that might
not exist; guard it with `??`, `isset()`, or `array_key_exists()`. In 0.4.x these keys were required and relation keys
were typed as model objects, which type-checked but didn't match the rows Yii returns.

Other inference changes in this release:

- A `find()` that returns a custom query class without `@template` or `@extends` PHPDoc, as Gii generates it, is bound
  to the model named at the call site: `Invoice::find()->one()` is `Invoice|null` and `Invoice::find()->all()` is
  `array<Invoice>` through the fluent methods inherited from `ActiveQuery` and through scope methods typed `: static` or
  `@return $this`, and `batch()` and `each()` yield `Invoice`. This works for a class name, a `class-string` variable,
  `self::find()`, `static::find()`, and `parent::find()`, and `CreditInvoice::find()` on a subclass yields
  `CreditInvoice`. PHPStan prints the query as `InvoiceQuery&yii\db\ActiveQuery<Invoice>`, also in error messages, and
  accepts it wherever `InvoiceQuery` or `ActiveQuery<Invoice>` is expected. With `reportWrongPhpDocTypeInVarTag`
  enabled, as `phpstan/phpstan-strict-rules` does, a redundant `/** @var InvoiceQuery $query */` on
  `$query = Invoice::find();` is now reported as `varTag.type`
  (`PHPDoc tag @var with type InvoiceQuery is not subtype of type InvoiceQuery&yii\db\ActiveQuery<Invoice>.`); remove
  the annotation, since the inferred type is more precise. Generic tags on the query class, a generic `@return` tag on
  `find()`, and edits to the `one()` and `all()` overrides that Gii generates are no longer needed; see
  [Custom Active Query classes](/docs/extensions/phpstan/examples/index.md#custom-active-query-classes) for what still needs PHPDoc.
- A query typed only as a query class without generic PHPDoc, such as a typed parameter or property, a value returned
  by a method, or the result of a scope typed `: self`, takes its model from its `one()` and `all()` overrides, or
  those of a parent query class, when they name exactly one Active Record class, as Gii's
  `@return Invoice|array|null` and `@return Invoice[]|array` do: `one()` is `Invoice|null`, `all()` is `array<Invoice>`,
  `batch()` and `each()` yield `Invoice`, and `asArray()` gives the `Invoice` row shape. A query class without such
  overrides, a union of different query classes, and a relation getter typed `@return \yii\db\ActiveQuery|CommentQuery`,
  as Gii writes it, keep PHPStan's own answer, such as `array|yii\db\ActiveRecord|null` for `one()`; declare
  `@extends \yii\db\ActiveQuery<Model>` on the query class, or a precise `@return` on the getter.
- Because `one()` and `all()` on these queries no longer include `array`, a defensive `is_array()` or `instanceof` check
  on their results, which 0.4.x typed as `mixed`, is now reported as always false or always true
  (`function.impossibleType`, `instanceof.alwaysTrue`); remove it. Rows are typed only through the value `asArray()`
  returns, not after a bare `$query->asArray();` statement or a switch to array mode inside the query class, and a
  model taken from the overrides belongs to the query class, not to the model the query object was built for. See
  [Custom Active Query classes](/docs/extensions/phpstan/examples/index.md#custom-active-query-classes).
- An `asArray()` row treats a `@property` tag whose type, ignoring `null`, is an Active Record model or an iterable of
  models as a relation: its key is optional and holds a related row (`array<string, mixed>|null`) or an array of
  related rows (`array<array<string, mixed>>`). Any other tag keeps its own type, as a required key when it's writable
  and as an optional key when it's read-only, and write-only tags give no key. A column tagged with a value object or an
  enum keeps that type as a required key, although the row holds the raw database value.
- Components and container services accept every definition form Yii accepts when its class can be determined, a
  `yii\di\Instance` reference in a container definition resolves through its ID, and a definition whose class can't
  be determined, such as a closure without a return type, no longer stops the analysis: `get()` returns `object` for it,
  and `Yii::$app->id` is `object` too. An `Instance` object used as a component is typed as `yii\di\Instance`, since the
  service locator returns that object directly. The unresolved `object` fallback applies even for a
  component the application class declares, unless it's configured as an array without a class key. A string definition, or the `class` or `__class` of an array
  definition, that names another container ID, such as `'mailer.alias' => 'mailer.real'`, resolves through that ID,
  following chains, and `get()` returns `object` for it when that ID's class can't be determined. Such a definition
  under a class name ID, such as `Foo::class => static fn() => new Bar()`, is `object` as well, no longer `Foo`. A list
  holding only a definition, such as `[['class' => Foo::class]]`, which Yii rejects, is no longer resolved. The
  exception `Please provide return type for '<id>' service closure.` no longer exists, and
  `Unsupported definition for '<id>'.` is thrown only for an integer, float, or boolean definition. See
  [Definition forms](/docs/extensions/phpstan/configuration/index.md#definition-forms).
- `asArray()` row shapes and `getAttribute()` types include the `@property` tags inherited from parent classes, used
  traits, and interfaces, so a model that extends a generated base class gets the tags of that class. The nearest
  declaration of a name wins, and tags declared by `yii\db\ActiveRecord` and the classes, interfaces, and traits it's
  built from aren't included.
- `getAttribute()` retains `mixed` for read-only tags, because Yii's attribute accessor doesn't invoke getters or load
  relations. Writable tags used for attribute inference must describe stored attributes.
- `asArray()` keeps the row shape through a nullsafe chain: `$user?->hasOne(Category::class, [...])->asArray()` is
  `ActiveQuery<array{...}>|null`.
- Calls that unpack any argument, such as `$headers->get('Accept', ...$args)`, `Target::filterMessages(...$args)`,
  `asArray(...$args)`, and `getAttribute(...$args)`, return the type Yii declares for the method. `get()` on the
  container or a service locator does so only when the id itself is unpacked, as in `$container->get(...$args)`; with
  only the arguments after the id unpacked, as in `$container->get('logger', ...$args)`, it still resolves the
  configured class.
- Each logger message tuple in `Logger::$messages` and `Target::$messages` is a `list`, so it's accepted where a `list`
  is expected; its printed shape doesn't change.
- Params whose keys contain quotes, backslashes, whitespace, line breaks, or other characters that aren't valid in an
  identifier get their exact shape. In 0.4.x, such keys could break the generated stub, and `Yii::$app->params` lost its
  shape. With no params configured, `Module::$params` stays `array`.

### Removed and changed API

These changes affect only code that extends or instantiates the extension classes directly. Class names are relative to
the `yii2\extensions\phpstan` namespace.

- `type\ActiveRecordDynamicMethodReturnTypeExtension` and `type\ActiveRecordDynamicStaticMethodReturnTypeExtension` are
  removed; delete any manual registration of them from your PHPStan configuration.
- `StubFilesExtension` is removed, together with its `stubDirectory` constructor argument; delete any manual
  registration of it. `stubs/yii.stub` and `type\ApplicationTypeNodeResolverExtension` replace the stub it generated.
- `ServiceMap::getComponentDefinitionByClassName()` is removed; use `ServiceMap::getComponentDefinitionById()`, which
  now omits the `__class` key as well as `class`.
- `ServiceMap::isUnresolvedComponent()` and `ServiceMap::isUnresolvedService()` are new; they return `true` for a
  component or container service that is configured but whose class can't be determined.
  `ServiceMap::isComponentWithoutClass()` is new too; it returns `true` for a component configured as an array without
  a class key, which Yii completes from the core components.
- `reflection\ComponentPropertyReflection::getType()` is removed; use `getReadableType()`.
- Constructors that changed, with their new parameter lists (`DeclarationDependencyTracker` is
  `PHPStan\Analyser\DeclarationDependencyTracker`):
  - `type\ActiveQueryDynamicMethodReturnTypeExtension`: `ReflectionProvider`, `PropertyTagTypeResolver` (replaces
    `FileTypeMapper`).
  - `type\ActiveRecordGetAttributeDynamicMethodReturnTypeExtension`: `ReflectionProvider`, `ServiceMap`,
    `PropertyTagTypeResolver` (replaces `FileTypeMapper`, which came before `ServiceMap`).
  - `type\ContainerDynamicMethodReturnTypeExtension`: `ServiceMap` (`ReflectionProvider` removed).
  - `type\ServiceLocatorDynamicMethodReturnTypeExtension`: `ServiceMap` (`ReflectionProvider` removed).
  - `property\ApplicationPropertiesClassReflectionExtension`: `AnnotationsPropertiesClassReflectionExtension`,
    `ReflectionProvider`, `ServiceMap`, `DeclarationDependencyTracker`, `array $genericComponents = []`
    (`DeclarationDependencyTracker` added).
  - `property\BehaviorPropertiesClassReflectionExtension`: `ReflectionProvider`, `ServiceMap`,
    `DeclarationDependencyTracker` (`AnnotationsPropertiesClassReflectionExtension` removed, `DeclarationDependencyTracker`
    added).
  - `method\BehaviorMethodsClassReflectionExtension`: `ReflectionProvider`, `ServiceMap`, `DeclarationDependencyTracker`
    (`DeclarationDependencyTracker` added).
- New public classes: `ParamsTypeBuilder`, `PropertyTagTypeResolver`, `ServiceMapResultCacheValueExtension`,
  `type\ActiveRecordQueryDynamicStaticMethodReturnTypeExtension`, and `type\ApplicationTypeNodeResolverExtension`.

Services registered through `extension.neon` are autowired and need no change.

### Result cache

The extension declares, through PHPStan 2.3 dependency tracking, what each analyzed file reads from your configuration:
a component or container service by id, and the behaviors of a class. It also records class dependencies on the
configured application class and on behavior classes. The application class and the params type are recorded instead on
the classes that declare `Yii::$app` and `Module::$params`, `yii\BaseYii` and `yii\base\Module`, and PHPStan propagates
them to every analyzed file that depends on either class or on a subclass. When the application class or the params
type changes, all those files are re-analyzed, including files that only override or inherit the properties. Editing the
configuration file, or a file it pulls in with `require` such as a params file, re-analyzes only the files that depend on
a changed value, and a change that leaves those values as they were, such as a param value of the same type, re-analyzes
nothing. You don't need to clear the cache (`clear-result-cache` or deleting `tmpDir`) after configuration changes.

[Back to documentation](/docs/extensions/phpstan/index.md#documentation)