You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
10
10
### Added
11
11
12
+
-**`whereHas` / `whereDoesntHave` closure parameter from relation traversal.** The closure parameter in `whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `withWhereHas`, and related methods is now typed as `Builder<RelatedModel>` instead of `Builder<CallingModel>`. The relation name string (first argument) is resolved by walking the model's relationship methods. Dot-notation chains like `whereHas('category.articles', fn($q) => ...)` follow each segment to the final related model. Works for static calls (`Brand::whereHas(...)`), builder instance calls (`$query->whereHas(...)`), and body-inferred relationships (no `@return` annotation). Falls back to the calling model's builder when the relation cannot be resolved.
12
13
-**Eloquent `where{PropertyName}()` dynamic methods.** Calls like `User::whereBrandId(42)` and `$query->whereLangCode('en')` now resolve instead of producing `unknown_member` diagnostics. For each known column on an Eloquent model (from `$casts`, `$attributes`, `$fillable`/`$guarded`/`$hidden`/`$appends`, `$dates`, timestamps, and `@property` annotations), a virtual `where{StudlyCase}()` method is synthesized on both the model (as a static method) and the Builder (as an instance method). Each method accepts a `mixed` value parameter and returns `Builder<ConcreteModel>`, so chains like `Bakery::whereFlour('rye')->whereApricot(true)->get()` resolve end-to-end.
13
14
-**Eloquent timestamp properties.** Models extending `Illuminate\Database\Eloquent\Model` now automatically get `created_at` and `updated_at` virtual properties typed as `Carbon\Carbon`. Setting `$timestamps = false` suppresses both. Overriding `CREATED_AT` or `UPDATED_AT` constants changes the column name; setting either to `null` disables that column. Explicit `$casts` entries (e.g. `'created_at' => 'immutable_datetime'`) take priority over the timestamp defaults.
14
15
-**Eloquent `$dates` array.** Legacy models using `protected $dates = [...]` now get `Carbon\Carbon`-typed virtual properties for each listed column. Explicit `$casts` entries take priority when both define the same column.
@@ -64,6 +65,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
64
65
-**Diagnostics now work for vendor files open in the editor.** Projects using `--prefer-source` or monorepo setups where libraries are edited from the `vendor/` directory no longer have diagnostics suppressed. Previously all vendor files were unconditionally skipped, hiding syntax errors, deprecation warnings, and all other diagnostics. The `analyze` command also accepts vendor paths as explicit overrides (e.g. `phpantom_lsp analyze vendor/some-package/src/`).
65
66
-**Full-line diagnostic suppression no longer requires a code mapping.** Full-line diagnostics (from tools like PHPStan that only report line numbers) are now suppressed whenever any precise diagnostic exists on the same line, regardless of error codes. Previously suppression relied on a hand-maintained mapping between error codes from different tools, which was incomplete and allowed redundant full-line underlines to obscure the precise location of the issue.
66
67
68
+
-**Hover on closure parameters lost inferred generic arguments.** When a closure parameter had an explicit bare class type hint (e.g. `function (Builder $q)`) and the callable signature inferred the same class with generic arguments (e.g. `Builder<Article>`), hover displayed the bare class name instead of the generic version. The inferred type string is now preserved through the resolution pipeline so that hover, completion, and diagnostics all see the same type.
69
+
-**Hover on variables assigned from chained methods lost generic parameters.** When a method returned `static`, `$this`, or `self` and the receiver carried generic parameters (e.g. `Builder<Article>`), hover on a variable assigned from the chain showed the bare class name (`Builder`) instead of the generic version. The receiver's type string is now carried through method call resolution so that self-referencing return types preserve generic parameters across chains.
70
+
-**Hover on the `$` sign of a variable at its assignment site showed no type.** Hovering on the `$` of `$order = new Order()` displayed a bare `$order` with no type information, while hovering one character to the right on `o` worked correctly. The variable's byte offset coincided with the assignment statement's start offset, causing the resolver to skip the assignment entirely. The cursor offset is now nudged past the statement boundary so the assignment is included.
67
71
-**Method chains through `__call` no longer lose the return type.** When a class defines `__call` and an unrecognized method is called (e.g. dynamic `whereColumn()` on an Eloquent Builder), the return type of `__call` is now used as a fallback. If `__call` returns `$this`, `static`, or `self`, the chain type is preserved so subsequent calls continue resolving. Previously the type was lost at the first dynamic call, breaking every link after it.
68
72
-**Null narrowing from `!== null` checks in conditions.** When a null-initialized variable was guarded by `$var !== null` in an `if` or `while` condition, the variable still showed `null` in its type inside the condition's `&&` operands and inside the then-body. The `!== null` check (and `!is_null()`, bare truthy guards) now narrows away `null` both for subsequent `&&` operands and inside the corresponding body block. Chained conditions like `$a !== null && $b !== null && $a->method()` narrow all checked variables. Conditions wrapped inside ternary expressions and return statements are also handled.
69
73
-**Variables assigned inside `if`/`while` conditions now resolve in the body.**`if ($admin = AdminUser::first())` and `while ($row = nextRow())` now register the assignment so the variable has a type inside the loop or branch body. Assignments wrapped in comparisons like `if (($conn = getConn()) !== null)` are also recognized.
| B6 |[Scope methods not found on Builder in analyzer chains](todo/bugs.md#b6-scope-methods-not-found-on-builder-in-analyzer-chains)| High | Medium |
27
-
| L11 |[`whereHas` closure parameter type from relation traversal](todo/laravel.md#l11-wherehas--wheredoesnthave-closure-parameter-type-from-relation-traversal)| Medium | Medium |
28
27
| B7 |[PHPDoc `@param` generic array type not merged with native `array` hint](todo/bugs.md#b7-phpdoc-param-generic-array-type-not-merged-with-native-array-hint)| Low | Medium |
| H10 |[`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union)| Medium | Medium |
0 commit comments