@@ -110,4 +110,177 @@ accessors, so in most cases the accessor method itself already produces
110110the virtual property. Parsing ` $appends ` would only help when the
111111accessor is defined in an unloaded parent class.
112112
113- ** Priority:** Low. The accessor method is the real source of truth.
113+ ** Priority:** Low. The accessor method is the real source of truth.
114+
115+ #### 5. ` *_count ` relationship count properties
116+
117+ Accessing ` $user->posts_count ` is a very common Laravel pattern
118+ (` withCount ` , ` loadCount ` , or eager-loaded counts). We don't
119+ synthesize these today.
120+
121+ ``` php
122+ $user->posts_count; // int, but we know nothing about it
123+ ```
124+
125+ Larastan handles this ** declaratively** — no call-site tracking
126+ required. When a property name ends with ` _count ` , it strips the
127+ suffix, checks whether the remainder (converted to camelCase) is a
128+ relationship method, and if so types the property as ` int ` .
129+
130+ ** Where to change:** In ` LaravelModelProvider::provide ` , after
131+ synthesizing relationship properties, iterate the relationship methods
132+ again and push a ` {snake_name}_count ` property typed as ` int ` for
133+ each one. The property should have lower priority than explicit
134+ ` @property ` tags.
135+
136+ ** Priority:** Medium. Simple to implement using the Larastan
137+ approach and covers a very common pattern.
138+
139+ #### 6. ` withSum() ` / ` withAvg() ` / ` withMin() ` / ` withMax() ` aggregate properties
140+
141+ Similar to ` withCount ` , these aggregate methods produce virtual
142+ properties named ` {relation}_{function} ` (e.g.
143+ ` Order::withSum('items', 'price') ` → ` $order->items_sum ` ). The same
144+ call-site tracking challenge applies, and the type depends on the
145+ aggregate function (` withSum ` /` withAvg ` → ` float ` ,
146+ ` withMin ` /` withMax ` → ` mixed ` ).
147+
148+ ** Priority:** Low. Unlike gap 5, these can't be inferred declaratively
149+ from the model alone — you'd need to track call-site string arguments.
150+ The ` @property ` workaround applies here too.
151+
152+ #### 7. ` $pivot ` property on BelongsToMany related models
153+
154+ When a model is accessed through a ` BelongsToMany ` (or ` MorphToMany ` )
155+ relationship, each related model instance gains a ` $pivot ` property at
156+ runtime that provides access to intermediate table columns.
157+
158+ ``` php
159+ /** @return BelongsToMany<Role , $this > */
160+ public function roles(): BelongsToMany {
161+ return $this->belongsToMany(Role::class)->withPivot('expires_at');
162+ }
163+
164+ $user->roles->first()->pivot; // Pivot instance — we know nothing about it
165+ $user->roles->first()->pivot->expires_at; // accessible at runtime, invisible to us
166+ ```
167+
168+ There are several layers of complexity here:
169+
170+ 1 . ** Basic ` $pivot ` property.** Related models accessed through a
171+ ` BelongsToMany ` or ` MorphToMany ` relationship should have a ` $pivot `
172+ property typed as ` \Illuminate\Database\Eloquent\Relations\Pivot `
173+ (or the custom pivot class when ` ->using(CustomPivot::class) ` is
174+ used). We don't currently synthesize this property at all.
175+
176+ 2 . ** ` withPivot() ` columns.** The ` withPivot('col1', 'col2') ` call
177+ declares which extra columns are available on the pivot object.
178+ Tracking these requires parsing the relationship method body for
179+ chained ` withPivot ` calls — similar in difficulty to the
180+ ` withCount ` call-site problem (gap 5).
181+
182+ 3 . ** Custom pivot models (` using() ` ).** When ` ->using(OrderItem::class) `
183+ is declared, the pivot is an instance of that custom class, which
184+ may have its own properties, casts, and accessors. Detecting this
185+ requires parsing the ` ->using() ` call in the relationship body.
186+
187+ Note: Larastan does ** not** handle pivot properties either — the
188+ ` $pivot ` property comes from Laravel's own ` @property ` annotations on
189+ the ` BelongsToMany ` relationship stubs. If the user's stub set
190+ includes these annotations, it already works through our PHPDoc
191+ provider.
192+
193+ ** Priority:** Low-medium. The basic ` $pivot ` typed as ` Pivot ` (layer 1)
194+ would be a modest improvement. Layers 2–3 require relationship body
195+ parsing that we don't currently do for this purpose. The ` @property `
196+ workaround on a custom Pivot class covers most real-world needs.
197+
198+ #### 8. Custom Eloquent builders (` HasBuilder ` / ` #[UseEloquentBuilder] ` )
199+
200+ Laravel 11+ introduced the ` HasBuilder ` trait and
201+ ` #[UseEloquentBuilder(UserBuilder::class)] ` attribute to let models
202+ declare a custom builder class. When present, ` User::query() ` and
203+ all static builder-forwarded calls should resolve to the custom
204+ builder instead of the base ` Illuminate\Database\Eloquent\Builder ` .
205+
206+ ``` php
207+ /** @extends Builder<User > */
208+ class UserBuilder extends Builder {
209+ /** @return $this */
210+ public function active(): static { ... }
211+ }
212+
213+ class User extends Model {
214+ /** @use HasBuilder<UserBuilder > */
215+ use HasBuilder;
216+ }
217+
218+ User::query()->active()->get(); // active() should resolve on UserBuilder
219+ ```
220+
221+ Larastan handles this via ` BuilderHelper::determineBuilderName() ` ,
222+ which inspects ` newEloquentBuilder() ` 's return type or the
223+ ` #[UseEloquentBuilder] ` attribute to find the custom builder class.
224+
225+ ** Where to change:** In ` build_builder_forwarded_methods ` , before
226+ loading the standard ` Eloquent\Builder ` , check whether the model
227+ declares a custom builder via ` @use HasBuilder<X> ` in ` use_generics `
228+ or a ` newEloquentBuilder() ` method with a non-default return type.
229+ If found, load and resolve that builder class instead.
230+
231+ ** Priority:** Medium. Custom builders are the recommended pattern
232+ for complex query scoping in modern Laravel and Larastan supports
233+ them. Without this, users of custom builders get no completions for
234+ their builder-specific methods via static calls on the model.
235+
236+ #### 9. ` #[Scope] ` attribute (Laravel 11+)
237+
238+ Laravel 11 introduced the ` #[Scope] ` attribute as an alternative to
239+ the ` scopeX ` naming convention. Methods decorated with ` #[Scope] `
240+ are available on the builder without needing the ` scope ` prefix:
241+
242+ ``` php
243+ class User extends Model {
244+ #[Scope]
245+ protected function active(Builder $query): void { ... }
246+ }
247+
248+ User::active()->get(); // works at runtime via #[Scope]
249+ ```
250+
251+ Larastan checks for this attribute in ` BuilderHelper::searchOnEloquentBuilder() ` .
252+ We currently only detect scopes via the ` scopeX ` naming convention in
253+ ` is_scope_method ` .
254+
255+ ** Where to change:** In the parser, extract ` #[Scope] ` attributes
256+ from method declarations. In ` LaravelModelProvider::provide ` , treat
257+ methods with the ` #[Scope] ` attribute the same as ` scopeX ` methods
258+ (strip the first ` $query ` parameter, expose as both static and
259+ instance virtual methods).
260+
261+ ** Priority:** Low-medium. The ` scopeX ` convention still works and
262+ is more common. The ` #[Scope] ` attribute is newer and adoption is
263+ growing.
264+
265+ #### 10. Higher-order collection proxies
266+
267+ Laravel collections support higher-order proxies via magic properties
268+ like ` $users->map->name ` or ` $users->filter->isActive() ` . These
269+ produce a ` HigherOrderCollectionProxy ` that delegates property
270+ access / method calls to each item in the collection.
271+
272+ ``` php
273+ $users->map->email; // Collection<int , string >
274+ $users->filter->isVerified(); // Collection<int , User >
275+ $users->each->notify(); // void (side-effect)
276+ ```
277+
278+ Larastan handles this with ` HigherOrderCollectionProxyPropertyExtension `
279+ and ` HigherOrderCollectionProxyExtension ` , which resolve the proxy's
280+ template types and delegate property/method lookups to the collection's
281+ value type.
282+
283+ ** Priority:** Low. This is a convenience syntax and most users use
284+ closures instead. Requires synthesizing virtual properties on
285+ collection classes that return a proxy type parameterised with the
286+ collection's value type.
0 commit comments