Skip to content

Commit b3e4596

Browse files
committed
Update Laravel todo with all known info
1 parent 842c154 commit b3e4596

1 file changed

Lines changed: 174 additions & 1 deletion

File tree

docs/todo-laravel.md

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,177 @@ accessors, so in most cases the accessor method itself already produces
110110
the virtual property. Parsing `$appends` would only help when the
111111
accessor 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

Comments
 (0)