Skip to content

Commit fe653d0

Browse files
Fix: Handle generic type components in ApplicationPropertiesClassReflectionExtension. (#59)
1 parent a2df0f8 commit fe653d0

10 files changed

+314
-219
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Bug #55: Remove `OS` and `PHP` version specifications from workflow files for simplification (@terabytesoftw)
2626
- Enh #56: Add `ServiceLocatorDynamicMethodReturnTypeExtension` to provide precise type inference for `get()` method (@terabytesoftw)
2727
- Bug #57: Clarify exception documentation and improve type inference descriptions in test cases (@terabytesoftw)
28+
- Bug #58: Handle generic type components in `ApplicationPropertiesClassReflectionExtension`.
2829

2930
## 0.2.3 June 09, 2025
3031

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ inference, dynamic method resolution, and comprehensive property reflection.
4444
**Application Component Resolution**
4545
- Automatic type inference for `Yii::$app->component` access.
4646
- Behavior property and method reflection.
47+
- Generic component support with configurable type parameters.
48+
- Non-destructive generic configuration - extend without overriding defaults.
4749
- Support for custom component configurations.
4850
- User component with `identity`, `id`, `isGuest` property types.
4951

@@ -90,6 +92,9 @@ parameters:
9092
9193
yii2:
9294
config_path: config/phpstan-config.php
95+
component_generics:
96+
user: identityClass # Built-in (already configured)
97+
repository: modelClass # Custom generic component
9398
```
9499

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

docs/configuration.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,176 @@ return [
206206
];
207207
```
208208

209+
### Generic component configuration
210+
211+
You can configure which property in the component definition contains the generic type information.
212+
213+
#### Default generic components
214+
215+
The extension comes with pre-configured generic support for common Yii components:
216+
217+
```neon
218+
parameters:
219+
yii2:
220+
component_generics:
221+
user: identityClass # Built-in: User<IdentityClass>
222+
```
223+
224+
#### Adding custom generic components
225+
226+
You can extend the generic configuration without overriding the defaults:
227+
228+
```neon
229+
parameters:
230+
yii2:
231+
component_generics:
232+
userRepository: modelClass # Add custom generic
233+
postCollection: elementType # Add another custom generic
234+
```
235+
236+
#### Service configuration
237+
238+
```php
239+
<?php
240+
241+
declare(strict_types=1);
242+
243+
// config/phpstan-config.php
244+
return [
245+
'components' => [
246+
'user' => [
247+
'class' => \yii\web\User::class,
248+
'identityClass' => \app\models\User::class, // Generic type parameter
249+
],
250+
'userRepository' => [
251+
'class' => \app\repositories\Repository::class,
252+
'modelClass' => \app\models\User::class, // Generic type parameter
253+
],
254+
'postCollection' => [
255+
'class' => \app\collections\Collection::class,
256+
'elementType' => \app\models\Post::class, // Generic type parameter
257+
],
258+
],
259+
];
260+
```
261+
262+
#### Usage with proper type inference
263+
264+
```php
265+
<?php
266+
267+
declare(strict_types=1);
268+
269+
use Yii;
270+
271+
class UserController
272+
{
273+
public function actionProfile(): string
274+
{
275+
// ✅ PHPStan knows this is User<app\models\User>
276+
$user = Yii::$app->user;
277+
278+
// ✅ PHPStan knows identity is app\models\User
279+
$identity = $user->identity;
280+
281+
// ✅ PHPStan knows this is Repository<app\models\User>
282+
$repository = Yii::$app->userRepository;
283+
284+
// ✅ PHPStan knows this is Collection<app\models\Post>
285+
$collection = Yii::$app->postCollection;
286+
287+
return $this->render('profile', ['user' => $identity]);
288+
}
289+
}
290+
```
291+
292+
#### Creating generic-aware components
293+
294+
For your custom components to work with generics, define them using PHPDoc annotations:
295+
296+
```php
297+
<?php
298+
299+
declare(strict_types=1);
300+
301+
namespace app\repositories;
302+
303+
use yii\base\Component;
304+
305+
/**
306+
* Generic repository component.
307+
*
308+
* @template T of \yii\db\ActiveRecord
309+
*/
310+
class Repository extends Component
311+
{
312+
/**
313+
* @phpstan-var class-string<T>
314+
*/
315+
public string $modelClass;
316+
317+
/**
318+
* @phpstan-return T|null
319+
*/
320+
public function findOne(int $id): \yii\db\ActiveRecord|null
321+
{
322+
return $this->modelClass::findOne($id);
323+
}
324+
325+
/**
326+
* @phpstan-return T[]
327+
*/
328+
public function findAll(): array
329+
{
330+
return $this->modelClass::find()->all();
331+
}
332+
}
333+
```
334+
335+
```php
336+
<?php
337+
338+
declare(strict_types=1);
339+
340+
namespace app\collections;
341+
342+
use yii\base\Component;
343+
344+
/**
345+
* Generic collection component.
346+
*
347+
* @template T
348+
*/
349+
class Collection extends Component
350+
{
351+
/**
352+
* @phpstan-var class-string<T>
353+
*/
354+
public string $elementType;
355+
356+
/**
357+
* @phpstan-var T[]
358+
*/
359+
private array $items = [];
360+
361+
/**
362+
* @phpstan-param T $item
363+
*/
364+
public function add($item): void
365+
{
366+
$this->items[] = $item;
367+
}
368+
369+
/**
370+
* @phpstan-return T[]
371+
*/
372+
public function getAll(): array
373+
{
374+
return $this->items;
375+
}
376+
}
377+
```
378+
209379
### Behavior configuration
210380

211381
Configure behaviors for proper method and property reflection.

docs/examples.md

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -345,35 +345,57 @@ class UserService
345345
}
346346
```
347347

348-
### Custom components
348+
### Custom components with generics
349349

350350
```php
351351
<?php
352352

353353
declare(strict_types=1);
354354

355-
// Component configuration in config/phpstan.php
355+
// Component configuration in config/phpstan-config.php
356356
return [
357357
'components' => [
358358
'paymentService' => [
359359
'class' => \app\services\PaymentService::class,
360360
],
361-
'imageProcessor' => [
362-
'class' => \app\services\ImageProcessor::class,
361+
'userRepository' => [
362+
'class' => \app\repositories\Repository::class,
363+
'modelClass' => \app\models\User::class,
364+
],
365+
'postRepository' => [
366+
'class' => \app\repositories\Repository::class,
367+
'modelClass' => \app\models\Post::class,
363368
],
364369
],
365370
];
371+
```
372+
373+
Generic configuration in phpstan.neon.
374+
375+
```neon
376+
parameters:
377+
yii2:
378+
component_generics:
379+
userRepository: modelClass
380+
postRepository: modelClass
381+
```
382+
383+
Usage in controllers and services:
384+
385+
```php
386+
<?php
387+
388+
declare(strict_types=1);
366389

367-
// Usage in controllers
368390
use Yii;
369391
use yii\web\Controller;
370392

371393
class PaymentController extends Controller
372394
{
373395
public function actionProcess(): array
374396
{
375-
// ✅ PHPStan knows this is PaymentService
376-
$paymentService = Yii::$app->paymentService; // PaymentService
397+
// ✅ PHPStan knows this is PaymentService (non-generic component)
398+
$paymentService = Yii::$app->paymentService;
377399

378400
$result = $paymentService->processPayment(
379401
[
@@ -386,22 +408,60 @@ class PaymentController extends Controller
386408
// ✅ PHPStan knows the return type based on method signature
387409
return $result; // array
388410
}
411+
}
412+
```
413+
414+
```php
415+
<?php
416+
417+
declare(strict_types=1);
418+
419+
use Yii;
420+
421+
class UserController extends Controller
422+
{
423+
public function actionIndex(): array
424+
{
425+
// ✅ PHPStan knows this is Repository<app\models\User> (generic component)
426+
$userRepository = Yii::$app->userRepository;
427+
428+
// ✅ PHPStan knows findAll() returns app\models\User[]
429+
$users = $userRepository->findAll();
430+
431+
// ✅ PHPStan knows this is Repository<app\models\Post> (generic component)
432+
$postRepository = Yii::$app->postRepository;
433+
434+
// ✅ PHPStan knows findOne() returns app\models\Post|null
435+
$post = $postRepository->findOne(1);
436+
437+
return $this->render(
438+
'index',
439+
[
440+
'users' => $users,
441+
'post' => $post,
442+
],
443+
);
444+
}
389445

390-
public function actionProcessImage(): string
446+
public function actionUserProfile(int $id): string
391447
{
392-
// ✅ PHPStan knows this is ImageProcessor
393-
$imageProcessor = Yii::$app->imageProcessor; // ImageProcessor
448+
// ✅ PHPStan knows this is Repository<app\models\User> (generic component)
449+
$repository = Yii::$app->userRepository;
394450

395-
$uploadedFile = \yii\web\UploadedFile::getInstanceByName('image');
451+
// ✅ PHPStan knows findOne() returns app\models\User|null
452+
$user = $repository->findOne($id); // app\models\User|null
396453

397-
if ($uploadedFile !== null) {
398-
// ✅ Method calls are typed
399-
$processedPath = $imageProcessor->resize($uploadedFile->tempName, 800, 600);
400-
401-
return $processedPath; // string
454+
if ($user === null) {
455+
throw new \yii\web\NotFoundHttpException('User not found');
402456
}
403457

404-
throw new \yii\web\BadRequestHttpException('No image uploaded');
458+
// ✅ PHPStan knows $user is app\models\User (not null)
459+
return $this->render(
460+
'profile',
461+
[
462+
'user' => $user,
463+
],
464+
);
405465
}
406466
}
407467
```
@@ -438,7 +498,7 @@ class ServiceManager
438498
// ✅ Type-safe service resolution
439499
$paymentService = $this->container->get(PaymentService::class); // PaymentService
440500
$emailService = $this->container->get(EmailService::class); // EmailService
441-
$cache = $this->container->get('cache'); // CacheService (if configured)
501+
$cache = $this->container->get('cache'); // CacheService (if configured) or mixed
442502

443503
$paymentResult = $paymentService->charge($orderData['total']);
444504

@@ -505,7 +565,7 @@ class CustomServiceManager extends ServiceLocator
505565

506566
declare(strict_types=1);
507567

508-
// config/phpstan.php - Container configuration
568+
// config/phpstan-config.php - Container configuration
509569
return [
510570
'container' => [
511571
'definitions' => [

extension.neon

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ parameters:
88

99
yii2:
1010
config_path: ''
11+
component_generics:
12+
user: identityClass
1113

1214
parametersSchema:
1315
yii2: structure(
1416
[
15-
config_path: schema(string())
17+
config_path: schema(string()),
18+
component_generics: schema(arrayOf(string(), string()))
1619
]
1720
)
1821

@@ -23,12 +26,11 @@ services:
2326
-
2427
class: yii2\extensions\phpstan\property\ApplicationPropertiesClassReflectionExtension
2528
tags: [phpstan.broker.propertiesClassReflectionExtension]
29+
arguments:
30+
genericComponents: %yii2.component_generics%
2631
-
2732
class: yii2\extensions\phpstan\property\BehaviorPropertiesClassReflectionExtension
2833
tags: [phpstan.broker.propertiesClassReflectionExtension]
29-
-
30-
class: yii2\extensions\phpstan\property\UserPropertiesClassReflectionExtension
31-
tags: [phpstan.broker.propertiesClassReflectionExtension]
3234
-
3335
class: yii2\extensions\phpstan\type\ActiveQueryDynamicMethodReturnTypeExtension
3436
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

0 commit comments

Comments
 (0)