Skip to content

Commit de696c3

Browse files
committed
Add Eloquent relation and column string completion
- Typing inside string arguments to Eloquent methods like with(), load(), whereHas(), and where() now offers completions for relationship method names (with dot-notation traversal) and column/attribute names. - Recognize Larastan's model-property<T> pseudo-type as a string subtype. - Remove completed L11 from todo files and add demos to examples/laravel/app/Demo.php.
1 parent 673af85 commit de696c3

12 files changed

Lines changed: 884 additions & 174 deletions

File tree

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- **Extract interface.** A new `refactor.extract` code action generates an interface from a concrete class. All public method signatures (excluding the constructor) are extracted into a new `{ClassName}Interface.php` file in the same directory, and the class is updated with `implements {ClassName}Interface`. Class-level and method-level `@template` tags are preserved when referenced by extracted methods.
1616
- **`@template` on `@method` tags.** Virtual methods declared via `@method` PHPDoc tags can now define their own template parameters using the `<T of Bound>` syntax (e.g. `@method TVal get<TVal of mixed>(TVal $default)`). Template inference at call sites works the same as for real methods.
1717
- **Laravel custom Eloquent builder support.** Models using the `#[UseEloquentBuilder]` attribute now have their custom builder's methods forwarded as static methods on the model. `query()`, `newQuery()`, and `newModelQuery()` return the custom builder type with correct generic model substitution. Contributed by @MingJen in https://github.com/AJenbo/phpantom_lsp/pull/118.
18+
- **Eloquent relation and column string completion.** Typing inside string arguments to `with()`, `load()`, `whereHas()`, and other Eloquent methods that accept relation names now offers relationship method names as completions, with dot-notation traversal for nested relations. Similarly, `where()`, `orderBy()`, `select()`, `pluck()`, and other column-accepting methods offer model column names (from `$casts`, `$fillable`, `@property` tags, timestamps, etc.).
19+
- **`model-property<T>` pseudo-type recognition.** The Larastan `model-property<Model>` type no longer triggers "unknown class" diagnostics. It is treated as a string subtype.
1820

1921
### Fixed
2022

docs/todo.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ within the same impact tier.
2525

2626
| # | Item | Impact | Effort |
2727
| --- | --------------------------------------------------------------------------------------------------------------------- | ---------- | ------ |
28+
| X4 | [Full background indexing](todo/indexing.md#x4-full-background-indexing) (workspace symbols, fast find-references) | Medium | High |
29+
| X1 | [Staleness detection and auto-refresh](todo/indexing.md#x1-staleness-detection-and-auto-refresh) | Medium | Medium |
30+
| L1 | [Facade completion](todo/laravel.md#l1-facade-completion) | High | High |
2831
| D10 | [PHPMD diagnostic proxy](todo/diagnostics.md#d10-phpmd-diagnostic-proxy) | Low | Medium |
2932

3033
## Sprint 7 — 1.0 release & IDE extensions
@@ -33,14 +36,10 @@ within the same impact tier.
3336
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
3437
| | Clear [refactoring gate](todo/refactor.md) |||
3538
| E5 | [Extension stub coverage audit](todo/external-stubs.md#e5-extension-stub-selection-stubs-extensions) | Medium | Low |
36-
| X1 | [Staleness detection and auto-refresh](todo/indexing.md#x1-staleness-detection-and-auto-refresh) | Medium | Medium |
3739
| E1 | [External stub packages (ide-helper, etc.)](todo/external-stubs.md#e1-project-level-phpstorm-stubs-for-gtd) | Medium-High | Medium |
3840
| E2 | [Project-level stubs as type resolution source](todo/external-stubs.md#e2-project-level-stubs-as-resolution-source) (depends on E1) | Medium | Medium |
3941
| E3 | [IDE-provided and `.phpantom.toml` stub paths](todo/external-stubs.md#e3-ide-provided-and-phpantomtoml-stub-paths) (depends on E2) | Low-Medium | Low |
4042
| E4 | [Stub version alignment with target PHP](todo/external-stubs.md#e4-embedded-stub-override-with-external-stubs) (depends on E1) | Medium | Medium |
41-
| L11 | [Relation dot-notation string and column name string completion](todo/laravel.md#l11-relation-dot-notation-string-completion-and-column-name-string-completion) | Medium-High | Medium-High |
42-
| X4 | [Full background indexing](todo/indexing.md#x4-full-background-indexing) (workspace symbols, fast find-references) | Medium | High |
43-
| L1 | [Facade completion](todo/laravel.md#l1-facade-completion) | High | High |
4443
| | **Release 1.0.0 + IDE extensions** | | |
4544

4645
## Sprint 8 — Blade support

docs/todo/laravel.md

Lines changed: 0 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -461,171 +461,6 @@ methods, or document this as a known limitation.
461461

462462
---
463463

464-
#### L11. Relation dot-notation string completion and column name string completion
465-
466-
**Impact: Medium-High · Effort: Medium-High**
467-
468-
Many Eloquent methods accept string arguments that refer to
469-
**relationship names** (with dot-notation for nested eager loads) or
470-
**column/attribute names**. PHPantom should offer completion inside
471-
these string literals.
472-
473-
##### Relation string completion
474-
475-
Eloquent methods like `with()`, `load()`, `has()`, `whereHas()`,
476-
`doesntHave()`, `whereDoesntHave()`, `withCount()`, `loadCount()`,
477-
`loadMissing()`, and `loadMorph()` accept relationship names as
478-
string arguments. These names correspond to public methods on the model
479-
that return a relationship instance (`HasMany`, `BelongsTo`, etc.).
480-
481-
Dot-notation chains traverse nested relationships:
482-
483-
```php
484-
// Eager-load: User → mother() → sister() → son()
485-
$users = User::with(['mother.sister.son'])->get();
486-
487-
// Constrained eager load:
488-
User::with(['posts' => function ($q) { ... }])->get();
489-
490-
// Nested with counts:
491-
User::withCount(['posts', 'comments.replies'])->get();
492-
```
493-
494-
**Implementation:**
495-
496-
1. Detect when the cursor is inside a string argument to one of the
497-
target methods (listed above) where the receiver is an Eloquent
498-
model or Builder.
499-
2. Resolve the model class from the receiver type (e.g. `User` from
500-
`User::with(...)` or from the Builder's generic parameter).
501-
3. Scan the model for public methods whose return type extends one of
502-
the relationship base classes (`HasOne`, `HasMany`, `BelongsTo`,
503-
`BelongsToMany`, `HasOneThrough`, `HasManyThrough`,
504-
`MorphOne`, `MorphMany`, `MorphTo`, `MorphToMany`,
505-
`MorphedByMany`).
506-
4. Offer those method names as completions.
507-
5. When the string already contains a dot (e.g. `'mother.si|'`),
508-
resolve the chain segment-by-segment: `mother()` returns
509-
`BelongsTo<Person>`, so look up `Person`'s relationship methods
510-
for the next segment.
511-
6. Inside array literals (`['posts', 'comments.|']`), trigger on each
512-
element independently. Also handle the `=>` closure form where the
513-
key is the relation string.
514-
515-
**Target methods (non-exhaustive):**
516-
517-
| Method | Where | Notes |
518-
|--------|-------|-------|
519-
| `with()` | Builder, Model | Eager loading |
520-
| `without()` | Builder | Remove eager load |
521-
| `load()` | Model | Lazy eager load |
522-
| `loadMissing()` | Model | Load if not loaded |
523-
| `loadCount()` | Model | Lazy count load |
524-
| `loadMorph()` | Model | Morph relation load |
525-
| `has()` | Builder | Existence query |
526-
| `orHas()` | Builder | OR existence |
527-
| `doesntHave()` | Builder | Non-existence |
528-
| `orDoesntHave()` | Builder | OR non-existence |
529-
| `whereHas()` | Builder | Constrained existence |
530-
| `orWhereHas()` | Builder | OR constrained |
531-
| `whereDoesntHave()` | Builder | Constrained non-existence |
532-
| `withCount()` | Builder | Count sub-select |
533-
| `withSum()` | Builder | Aggregate |
534-
| `withAvg()` | Builder | Aggregate |
535-
| `withMin()` | Builder | Aggregate |
536-
| `withMax()` | Builder | Aggregate |
537-
| `withExists()` | Builder | Existence flag |
538-
539-
##### Column name string completion
540-
541-
Several Eloquent and Query Builder methods accept column name strings.
542-
PHPantom already synthesizes `where{PropertyName}()` dynamic methods
543-
from model properties, but the same property names should also be
544-
offered as string completions inside methods that take column
545-
arguments:
546-
547-
```php
548-
// Column name as string:
549-
User::where('middle_name', 'like', '%foo%');
550-
User::whereIn('status', ['active', 'pending']);
551-
User::orderBy('created_at');
552-
User::select(['id', 'name', 'email']);
553-
User::pluck('email');
554-
555-
// Also in update/insert arrays (keys are column names):
556-
$user->update(['middle_name' => 'Jane']);
557-
```
558-
559-
**Implementation:**
560-
561-
1. Detect when the cursor is inside a string argument to a method on
562-
a Builder or Model where the parameter represents a column name.
563-
The parameter name or position in the known method signatures
564-
identifies column-position arguments (e.g. the first argument to
565-
`where()`, `whereIn()`, `orderBy()`, `groupBy()`, `select()`,
566-
`pluck()`, `value()`, `increment()`, `decrement()`, etc.).
567-
2. Resolve the model class from the Builder's generic parameter.
568-
3. Collect all known property names from the model: `$casts`,
569-
`$fillable`, `$guarded`, `$hidden`, `$visible`, `$attributes`
570-
defaults, `@property` annotations, accessor methods, and
571-
timestamp columns.
572-
4. Offer those names as string completions.
573-
574-
**Target methods (non-exhaustive):**
575-
576-
| Method | Column parameter position |
577-
|--------|--------------------------|
578-
| `where()` | 1st argument |
579-
| `orWhere()` | 1st argument |
580-
| `whereIn()` | 1st argument |
581-
| `whereNotIn()` | 1st argument |
582-
| `whereBetween()` | 1st argument |
583-
| `whereNull()` | 1st argument |
584-
| `whereNotNull()` | 1st argument |
585-
| `orderBy()` | 1st argument |
586-
| `orderByDesc()` | 1st argument |
587-
| `groupBy()` | all arguments |
588-
| `having()` | 1st argument |
589-
| `select()` | all arguments |
590-
| `addSelect()` | all arguments |
591-
| `pluck()` | 1st argument |
592-
| `value()` | 1st argument |
593-
| `increment()` | 1st argument |
594-
| `decrement()` | 1st argument |
595-
| `latest()` | 1st argument |
596-
| `oldest()` | 1st argument |
597-
598-
##### `model-property<T>` type resolution
599-
600-
Larastan defines a `model-property<Model>` pseudo-type that represents
601-
the union of column/attribute names on a given Eloquent model as string
602-
literals. PHPantom currently does not recognize this type, which
603-
produces false "unknown class" diagnostics when it appears in
604-
docblocks (see [#33](https://github.com/AJenbo/phpantom_lsp/issues/33)).
605-
606-
```php
607-
/** @return array{process: array<model-property<Process>, mixed>} */
608-
public function broadcastWith(): array { return []; }
609-
```
610-
611-
This should be handled as part of L11 since the column name knowledge
612-
is the same data:
613-
614-
1. **Minimum viable:** Recognize `model-property<T>` as a `string`
615-
subtype so it never triggers an "unknown class" diagnostic.
616-
2. **Full resolution:** When the generic argument resolves to an
617-
Eloquent model, expand `model-property<Process>` to the concrete
618-
union of known property names (from `$casts`, `$fillable`,
619-
`@property`, accessors, timestamps, etc.). This enables type
620-
checking at call sites and connects directly to the column name
621-
list that L11's string completion already builds.
622-
623-
**Relationship with L1 (Facade completion):** This feature is
624-
independent and can be implemented before or after L1. The column
625-
name source (model property list) is the same data that
626-
`LaravelModelProvider` already collects for virtual property
627-
synthesis and `where{PropertyName}()` dynamic methods.
628-
629464
#### L12. `HasUuids` / `HasUlids` trait — `$id` typed as `string`
630465

631466
**Impact: Low-Medium · Effort: Low**

examples/laravel/app/Demo.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,23 @@ public function phpdocVirtualMembers(): void
232232
}
233233

234234

235+
// ── Eloquent Relation & Column String Completion ────────────────────────
236+
// Trigger completion inside the string arguments below.
237+
238+
public function eloquentStringCompletion(): void
239+
{
240+
// Relation string completion in with(), load(), has(), etc.
241+
BlogAuthor::with(''); // offers: posts, profile, …
242+
BlogPost::with(''); // offers: author, comments, …
243+
BlogAuthor::with('posts.'); // dot-notation: offers nested relations on BlogPost
244+
245+
// Column name completion in where(), orderBy(), select(), etc.
246+
BlogAuthor::where(''); // offers: name, email, active, genre, …
247+
BlogPost::orderBy(''); // offers: title, published, author_id, …
248+
Bakery::select(''); // offers: flour, apricot, kitchen_id, …
249+
}
250+
251+
235252
// ── Laravel Config (definition & references) ────────────────────────
236253

237254
public function laravelConfig(): void

0 commit comments

Comments
 (0)