Skip to content

Commit ad8bed7

Browse files
feat: Add ActiveRecordGetAttributeDynamicMethodReturnTypeExtension to provide precise type inference for getAttribute() method calls based on PHPDoc annotations. (#47)
1 parent 69c7317 commit ad8bed7

14 files changed

+778
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Bug #44: Move `UserPropertiesClassReflectionExtension` to `property` directory and add testing (@terabytesoftw)
1515
- Bug #45: Improve `ServiceMap` configuration for application types (`Base`, `Console`, `Web`) (@terabytesoftw)
1616
- Bug #46: Update `README.md` to enhance clarity and structure of `docs/installation.md`, `docs/configuration.md` and `docs/examples.md` (@terabytesoftw)
17+
- Enh #47: Add `ActiveRecordGetAttributeDynamicMethodReturnTypeExtension` to provide precise type inference for `getAttribute()` method calls based on PHPDoc annotations (@terabytesoftw)
1718

1819
## 0.2.3 June 09, 2025
1920

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ composer require --dev yii2-extensions/phpstan
4141
- Array/object result type inference based on `asArray()` usage.
4242
- Dynamic return type inference for `find()`, `findOne()`, `findAll()` methods.
4343
- Generic type support for `ActiveQuery<Model>` with proper chaining.
44+
- Hierarchical type resolution: model properties take precedence over behavior properties.
45+
- Precise type inference for `getAttribute()` method calls based on PHPDoc annotations.
46+
- Property type resolution from both model classes and attached behaviors.
4447
- Relation methods (`hasOne()`, `hasMany()`) with accurate return types.
48+
- Support for behavior property definitions through ServiceMap integration.
4549

4650
**Application Component Resolution**
4751
- Automatic type inference for `Yii::$app->component` access.
@@ -119,6 +123,12 @@ $query = User::find()->where(['active' => 1])->orderBy('name');
119123

120124
// ✅ Array results properly typed as array{id: int, name: string}[]
121125
$userData = User::find()->asArray()->all();
126+
127+
// ✅ Properly typed based on model property annotations string
128+
$userName = $user->getAttribute('name');
129+
130+
// ✅ Behavior property resolution string
131+
$slug = $user->getAttribute('slug');
122132
```
123133

124134
### Application Components

docs/configuration.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,28 @@ return [
209209
];
210210
```
211211

212+
### Behavior PHPDoc Requirements
213+
214+
For accurate type inference, behaviors should define their properties using PHPDoc.
215+
216+
```php
217+
<?php
218+
219+
use yii\base\Behavior;
220+
use yii\db\ActiveRecord;
221+
222+
/**
223+
* @template T of ActiveRecord
224+
* @extends Behavior<T>
225+
*
226+
* @property int $depth
227+
* @property int $lft
228+
* @property int $rgt
229+
* @property int|false $tree
230+
*/
231+
class NestedSetsBehavior extends Behavior {}
232+
```
233+
212234
### Container Configuration
213235

214236
Define DI container services.

docs/examples.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,75 @@ class ServiceFactory
552552

553553
## Behavior Examples
554554

555+
### Dynamic Attribute Type Inference
556+
557+
```php
558+
<?php
559+
560+
use yii\behaviors\Behavior;
561+
use yii\db\ActiveRecord;
562+
563+
/**
564+
* Behavior with PHPDoc property definitions.
565+
*
566+
* @template T of ActiveRecord
567+
* @extends Behavior<T>
568+
*
569+
* @property int $lft
570+
* @property int $rgt
571+
* @property int $depth
572+
* @property int|false $tree
573+
*/
574+
class NestedSetsBehavior extends Behavior
575+
{
576+
/** @phpstan-var 'lft' */
577+
public string $leftAttribute = 'lft';
578+
579+
/** @phpstan-var 'rgt' */
580+
public string $rightAttribute = 'rgt';
581+
582+
/** @phpstan-var 'depth' */
583+
public string $depthAttribute = 'depth';
584+
585+
public function moveAsRoot(): bool
586+
{
587+
// ✅ PHPStan now knows these are int types
588+
$leftValue = $this->getOwner()->getAttribute($this->leftAttribute); // int
589+
$rightValue = $this->getOwner()->getAttribute($this->rightAttribute); // int
590+
$depthValue = $this->getOwner()->getAttribute($this->depthAttribute); // int
591+
592+
// No more manual casting needed!
593+
return $this->performMove($leftValue, $rightValue, $depthValue);
594+
}
595+
}
596+
597+
class Category extends ActiveRecord
598+
{
599+
public function behaviors(): array
600+
{
601+
return [
602+
'nestedSets' => [
603+
'class' => NestedSetsBehavior::class,
604+
],
605+
];
606+
}
607+
}
608+
609+
class CategoryService
610+
{
611+
public function getNodeInfo(Category $category): array
612+
{
613+
// ✅ PHPStan knows these are int types from behavior
614+
return [
615+
'left' => $category->getAttribute('lft'), // int
616+
'right' => $category->getAttribute('rgt'), // int
617+
'depth' => $category->getAttribute('depth'), // int
618+
'tree' => $category->getAttribute('tree'), // int|false
619+
];
620+
}
621+
}
622+
```
623+
555624
### Property and Method Access through Behaviors
556625

557626
```php

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ services:
3838
-
3939
class: yii2\extensions\phpstan\type\ActiveRecordDynamicStaticMethodReturnTypeExtension
4040
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
41+
-
42+
class: yii2\extensions\phpstan\type\ActiveRecordGetAttributeDynamicMethodReturnTypeExtension
43+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
4144
-
4245
class: yii2\extensions\phpstan\type\ContainerDynamicMethodReturnTypeExtension
4346
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

0 commit comments

Comments
 (0)