Migration guide
هذا المحتوى غير متوفر بلغتك بعد.
PHP 8.3 requirement
Section titled “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
Section titled “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.
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.notFoundfor 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 tostubFiles; this repository does so withtests/support/application-classes.stub, registered inphpstan.neon.missingType.iterableValueat level 6 or higher when noparamsare configured. Configure at least one param, or include the extension throughvendor/.
Inferred type changes
Section titled “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@templateor@extendsPHPDoc, as Gii generates it, is bound to the model named at the call site:Invoice::find()->one()isInvoice|nullandInvoice::find()->all()isarray<Invoice>through the fluent methods inherited fromActiveQueryand through scope methods typed: staticor@return $this, andbatch()andeach()yieldInvoice. This works for a class name, aclass-stringvariable,self::find(),static::find(), andparent::find(), andCreditInvoice::find()on a subclass yieldsCreditInvoice. PHPStan prints the query asInvoiceQuery&yii\db\ActiveQuery<Invoice>, also in error messages, and accepts it whereverInvoiceQueryorActiveQuery<Invoice>is expected. WithreportWrongPhpDocTypeInVarTagenabled, asphpstan/phpstan-strict-rulesdoes, a redundant/** @var InvoiceQuery $query */on$query = Invoice::find();is now reported asvarTag.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@returntag onfind(), and edits to theone()andall()overrides that Gii generates are no longer needed; see 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 itsone()andall()overrides, or those of a parent query class, when they name exactly one Active Record class, as Gii’s@return Invoice|array|nulland@return Invoice[]|arraydo:one()isInvoice|null,all()isarray<Invoice>,batch()andeach()yieldInvoice, andasArray()gives theInvoicerow 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 asarray|yii\db\ActiveRecord|nullforone(); declare@extends \yii\db\ActiveQuery<Model>on the query class, or a precise@returnon the getter. - Because
one()andall()on these queries no longer includearray, a defensiveis_array()orinstanceofcheck on their results, which 0.4.x typed asmixed, is now reported as always false or always true (function.impossibleType,instanceof.alwaysTrue); remove it. Rows are typed only through the valueasArray()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. - An
asArray()row treats a@propertytag whose type, ignoringnull, 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\Instancereference 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()returnsobjectfor it, andYii::$app->idisobjecttoo. AnInstanceobject used as a component is typed asyii\di\Instance, since the service locator returns that object directly. The unresolvedobjectfallback applies even for a component the application class declares, unless it’s configured as an array without a class key. A string definition, or theclassor__classof an array definition, that names another container ID, such as'mailer.alias' => 'mailer.real', resolves through that ID, following chains, andget()returnsobjectfor it when that ID’s class can’t be determined. Such a definition under a class name ID, such asFoo::class => static fn() => new Bar(), isobjectas well, no longerFoo. A list holding only a definition, such as[['class' => Foo::class]], which Yii rejects, is no longer resolved. The exceptionPlease provide return type for '<id>' service closure.no longer exists, andUnsupported definition for '<id>'.is thrown only for an integer, float, or boolean definition. See Definition forms. asArray()row shapes andgetAttribute()types include the@propertytags 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 byyii\db\ActiveRecordand the classes, interfaces, and traits it’s built from aren’t included.getAttribute()retainsmixedfor 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()isActiveQuery<array{...}>|null.- Calls that unpack any argument, such as
$headers->get('Accept', ...$args),Target::filterMessages(...$args),asArray(...$args), andgetAttribute(...$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::$messagesandTarget::$messagesis alist, so it’s accepted where alistis 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->paramslost its shape. With no params configured,Module::$paramsstaysarray.
Removed and changed API
Section titled “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\ActiveRecordDynamicMethodReturnTypeExtensionandtype\ActiveRecordDynamicStaticMethodReturnTypeExtensionare removed; delete any manual registration of them from your PHPStan configuration.StubFilesExtensionis removed, together with itsstubDirectoryconstructor argument; delete any manual registration of it.stubs/yii.stubandtype\ApplicationTypeNodeResolverExtensionreplace the stub it generated.ServiceMap::getComponentDefinitionByClassName()is removed; useServiceMap::getComponentDefinitionById(), which now omits the__classkey as well asclass.ServiceMap::isUnresolvedComponent()andServiceMap::isUnresolvedService()are new; they returntruefor a component or container service that is configured but whose class can’t be determined.ServiceMap::isComponentWithoutClass()is new too; it returnstruefor a component configured as an array without a class key, which Yii completes from the core components.reflection\ComponentPropertyReflection::getType()is removed; usegetReadableType().- Constructors that changed, with their new parameter lists (
DeclarationDependencyTrackerisPHPStan\Analyser\DeclarationDependencyTracker):type\ActiveQueryDynamicMethodReturnTypeExtension:ReflectionProvider,PropertyTagTypeResolver(replacesFileTypeMapper).type\ActiveRecordGetAttributeDynamicMethodReturnTypeExtension:ReflectionProvider,ServiceMap,PropertyTagTypeResolver(replacesFileTypeMapper, which came beforeServiceMap).type\ContainerDynamicMethodReturnTypeExtension:ServiceMap(ReflectionProviderremoved).type\ServiceLocatorDynamicMethodReturnTypeExtension:ServiceMap(ReflectionProviderremoved).property\ApplicationPropertiesClassReflectionExtension:AnnotationsPropertiesClassReflectionExtension,ReflectionProvider,ServiceMap,DeclarationDependencyTracker,array $genericComponents = [](DeclarationDependencyTrackeradded).property\BehaviorPropertiesClassReflectionExtension:ReflectionProvider,ServiceMap,DeclarationDependencyTracker(AnnotationsPropertiesClassReflectionExtensionremoved,DeclarationDependencyTrackeradded).method\BehaviorMethodsClassReflectionExtension:ReflectionProvider,ServiceMap,DeclarationDependencyTracker(DeclarationDependencyTrackeradded).
- New public classes:
ParamsTypeBuilder,PropertyTagTypeResolver,ServiceMapResultCacheValueExtension,type\ActiveRecordQueryDynamicStaticMethodReturnTypeExtension, andtype\ApplicationTypeNodeResolverExtension.
Services registered through extension.neon are autowired and need no change.
Result cache
Section titled “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.