|
1 | 1 | # PHPantom — Laravel Support: Remaining Work |
2 | 2 |
|
3 | | -> Last updated: 2026-02-27 |
| 3 | +> Last updated: 2026-02-28 |
4 | 4 |
|
5 | 5 | This document tracks bugs, known gaps, and missing features in |
6 | 6 | PHPantom's Laravel Eloquent support. For the general architecture and |
@@ -283,4 +283,90 @@ value type. |
283 | 283 | **Priority:** Low. This is a convenience syntax and most users use |
284 | 284 | closures instead. Requires synthesizing virtual properties on |
285 | 285 | collection classes that return a proxy type parameterised with the |
286 | | -collection's value type. |
| 286 | +collection's value type. |
| 287 | + |
| 288 | +#### 11. `collect()` and other helper functions lose generic type info |
| 289 | + |
| 290 | +Laravel's `collect()` helper is annotated with function-level |
| 291 | +`@template` parameters: |
| 292 | + |
| 293 | +```php |
| 294 | +/** |
| 295 | + * @template TKey of array-key |
| 296 | + * @template TValue |
| 297 | + * @param array<TKey, TValue> $value |
| 298 | + * @return \Illuminate\Support\Collection<TKey, TValue> |
| 299 | + */ |
| 300 | +function collect($value = []) { ... } |
| 301 | +``` |
| 302 | + |
| 303 | +We correctly resolve the return type as `Collection`, but the |
| 304 | +generic arguments `TKey` and `TValue` are lost — the result is an |
| 305 | +unparameterised `Collection`, so `$users = collect($array)` followed |
| 306 | +by `$users->first()->` produces no completions for the element type. |
| 307 | + |
| 308 | +**Root cause:** `FunctionInfo` has no `template_params` or |
| 309 | +`template_bindings` fields (unlike `MethodInfo`, which has both). |
| 310 | +The `synthesize_template_conditional` function only handles the |
| 311 | +narrow pattern `@return T` where `T` is a bare template param bound |
| 312 | +via `@param class-string<T>`. It does **not** handle `@return |
| 313 | +Collection<TKey, TValue>` where multiple template params appear |
| 314 | +inside a generic return type. |
| 315 | + |
| 316 | +This affects every Laravel helper that uses function-level generics: |
| 317 | +`collect()`, `value()`, `retry()`, `tap()`, `with()`, `transform()`, |
| 318 | +`data_get()`, plus non-Laravel functions with the same pattern. |
| 319 | + |
| 320 | +**Where to change:** Add `template_params: Vec<String>` and |
| 321 | +`template_bindings: Vec<(String, String)>` to `FunctionInfo` (mirror |
| 322 | +the existing fields on `MethodInfo`). Populate them in |
| 323 | +`parser/functions.rs` from `@template` and `@param` annotations. |
| 324 | +In `resolve_rhs_function_call` (in `variable_resolution.rs`), after |
| 325 | +loading the `FunctionInfo`, build a substitution map from template |
| 326 | +bindings → call-site argument types and apply it to the return type |
| 327 | +before passing it to `type_hint_to_classes`. See the general TODO |
| 328 | +item (§ PHP Language Feature Gaps, "Function-level `@template` |
| 329 | +generic resolution") for the full implementation plan. |
| 330 | + |
| 331 | +**Priority:** Medium-high. `collect()` alone is used in virtually |
| 332 | +every Laravel codebase, and the loss of element types breaks |
| 333 | +completion chains on the resulting collection. |
| 334 | + |
| 335 | +#### 12. `$this` in inferred callable parameter types resolves to wrong class |
| 336 | + |
| 337 | +When a closure parameter is untyped and the inference system extracts |
| 338 | +callable param types from the called method's signature, `$this` in |
| 339 | +the extracted type resolves to the **calling class** (the class |
| 340 | +containing the user's code) instead of the class that declares the |
| 341 | +method. |
| 342 | + |
| 343 | +```php |
| 344 | +// Builder::when() signature (from Conditionable trait): |
| 345 | +// @param callable($this, mixed): $this $callback |
| 346 | + |
| 347 | +// In a controller: |
| 348 | +User::when($active, function ($query) { |
| 349 | + $query-> // $query inferred as Controller, not Builder<User> |
| 350 | +}); |
| 351 | +``` |
| 352 | + |
| 353 | +The callable param types are extracted as raw strings by |
| 354 | +`extract_callable_param_types`. When `$this` appears in these |
| 355 | +strings, `resolve_closure_params_with_inferred` passes them to |
| 356 | +`type_hint_to_classes`, which resolves `$this` relative to |
| 357 | +`ctx.current_class` — the class the user is editing, not the class |
| 358 | +that owns the method. |
| 359 | + |
| 360 | +In practice, most users type-hint the closure parameter explicitly |
| 361 | +(`function (Builder $query) { ... }`), which bypasses the inference |
| 362 | +entirely. The gap only manifests for untyped closure params. |
| 363 | + |
| 364 | +**Where to change:** In `infer_callable_params_from_receiver` (and |
| 365 | +the static variant), after extracting callable param types, replace |
| 366 | +any literal `$this` or `static` tokens with the FQN of the receiver |
| 367 | +class before returning them. This ensures the inferred types |
| 368 | +reference the declaring class rather than the calling class. |
| 369 | + |
| 370 | +**Priority:** Low. The explicit type-hint workaround is standard |
| 371 | +practice, and most IDE-aware codebases already type their closure |
| 372 | +parameters. |
0 commit comments