Skip to content

Commit dc7887d

Browse files
committed
Laravel accessor and mutator
1 parent 32da761 commit dc7887d

7 files changed

Lines changed: 980 additions & 360 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ PHPantom focuses on completion and go-to-definition and aims to do them really w
1818
| `@mixin` completion || 💰 ||| 🚧 |
1919
| `@phpstan` annotations ||| 🚧 || 🚧 |
2020
| Conditional return types ||||| 🚧 |
21-
| Laravel Eloquent ||||| |
21+
| Laravel Eloquent ||||| 🧩 |
2222
| Array shape inference ||||| 🚧 |
2323
| Object shape completion ||||| 🚧 |
2424
| Generator body types | 🚧 || 🚧 |||

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Laravel accessor and mutator virtual properties.** Eloquent models with legacy accessors (`getFullNameAttribute()`) or modern Laravel 9+ accessors (methods returning `Illuminate\Database\Eloquent\Casts\Attribute`) now produce virtual properties. The property name is derived by converting the method name to snake_case: `getFullNameAttribute` produces `$full_name`, and `avatarUrl()` produces `$avatar_url`. Legacy accessors use the method's declared return type; modern accessors use `mixed`. Works alongside relationship properties, scope methods, and builder forwarding. `getAttribute()` (a real Eloquent method) is correctly excluded.
13+
1214
- **Virtual member provider abstraction.** Introduced the `VirtualMemberProvider` trait and `VirtualMembers` struct in a new `virtual_members` module. This provides a priority-ordered pipeline for synthesizing members from `@method`/`@property` tags, `@mixin` classes, and framework-specific patterns (e.g. Laravel relationships, scopes, Builder forwarding). All completion and go-to-definition call sites now use the new `resolve_class_fully` entry point, which applies base inheritance resolution followed by virtual member providers. No providers are registered yet, so behavior is unchanged. This is the foundation for upcoming Laravel support.
1315
- **Laravel relationship properties.** Classes extending `Illuminate\Database\Eloquent\Model` now get virtual properties synthesized from relationship methods. A method returning `HasMany<Post, $this>` produces a `$posts` property typed as `\Illuminate\Database\Eloquent\Collection<Post>`, and `HasOne<Profile, $this>` produces a `$profile` property typed as `Profile`. Supports `HasOne`, `HasMany`, `BelongsTo`, `BelongsToMany`, `MorphOne`, `MorphMany`, `MorphTo`, `MorphToMany`, and `HasManyThrough`. Generic type parameters are extracted from Larastan-style `@return` annotations. The synthesized properties sit at the highest virtual member priority, beating `@property` tags from ide-helper and `@mixin` members. Works with `$this->`, instance variables, relationship methods defined in traits, indirect Model subclasses (through a BaseModel), fully-qualified return types, and cross-file PSR-4 resolution. Chaining through relationship properties (e.g. `$user->profile->getBio()`) resolves to the related model's members.
1416
- **Laravel scope methods.** Methods starting with `scope` on Eloquent model classes (e.g. `scopeActive`, `scopeOfType`) now produce virtual methods with the prefix stripped and the first letter lowercased (`active`, `ofType`). The first `$query` parameter is removed since Laravel injects it automatically. Scope methods are available as both static and instance methods, so `User::active()` and `$user->active()` both resolve. If the scope method returns `void` or has no return type, the synthesized method defaults to `\Illuminate\Database\Eloquent\Builder<static>`. Custom return types and extra parameters beyond `$query` are preserved.
@@ -37,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3739

3840
### Fixed
3941

42+
- **Relationship property collection type uses the related model's custom collection.** When a model like `Product` had a `HasMany<Review, $this>` relationship, the virtual property `$product->reviews` incorrectly used the owning model's custom collection (`ProductCollection`) instead of the related model's (`ReviewCollection`). The provider now loads the related model via `class_loader` and reads its `custom_collection` field. The integration test was updated to use distinct collections per model so the bug is no longer masked.
43+
4044
- **Static call chain property access.** Chains like `User::where('active', 1)->first()->profile->` now correctly resolve through the entire chain. Previously the resolver's enum-case detection (`ClassName::CaseName->`) was too greedy and matched any subject containing `::`, discarding the method chain and resolving to the class before `::`. For example, `BlogAuthor::whereIn(...)->first()->posts->` resolved to `BlogAuthor` instead of `Collection<BlogPost>`, causing `->posts->posts->posts` to repeat infinitely. The fix restricts the enum-case branch to subjects where the part after `::` contains no `->`, and adds a new property-chain handler for non-variable bases so that `ClassName::method()->prop->` splits and resolves correctly.
4145
- **Namespaced functions treated as global.** Functions declared inside a namespace (e.g. `Illuminate\Support\enum_value`) previously appeared in completion as bare `enum_value`, and accepting them produced `use function enum_value;` instead of `use function Illuminate\Support\enum_value;`. The completion builder is now namespace-aware. In `use function` context the insert text is the FQN. In inline code the short name is inserted and a `use function FQN;` auto-import is added via `additional_text_edits`, mirroring how class auto-import works. The detail field shows the namespace (e.g. `function (Illuminate\Support)`) so users can distinguish same-named functions from different namespaces. Functions in the same namespace as the current file do not receive an auto-import. Short-name collisions between namespaces are eliminated: deduplication now uses the FQN, and the redundant short-name fallback entry in `global_functions` has been removed.
4246
- **Docblock annotation scope leak across sibling methods.** `find_iterable_raw_type_in_source` could pick up a `@param` or `@var` annotation from a sibling method when the parameter or variable shared the same name. For example, if `fromParam(array $config)` had a `@param array{host: string} $config` annotation and a sibling `nestedLiteral()` method also used a local `$config`, the annotation from `fromParam` would shadow the local assignment. The backward scanner now tracks when it has exited the current scope and re-entered a sibling scope, skipping all annotations from that point on.

0 commit comments

Comments
 (0)