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/ARCHITECTURE.md
+20-3Lines changed: 20 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -551,9 +551,14 @@ resolve_class_fully(class)
551
551
├── 1. resolve_class_with_inheritance(class)
552
552
│ └── Returns base-resolved ClassInfo
553
553
│
554
-
└── 2. For each provider (in priority order):
555
-
└── if applies_to(class): merge provide(class) into result
556
-
└── Skips members that already exist (no overwrites)
554
+
├── 2. For each provider (in priority order):
555
+
│ └── if applies_to(class): merge provide(class) into result
556
+
│ └── Skips members that already exist (no overwrites)
557
+
│
558
+
├── 3. Merge members from implemented interfaces
559
+
│
560
+
└── 4. apply_laravel_patches(class, fqn)
561
+
└── Post-resolution fixups for Laravel classes (see below)
557
562
```
558
563
559
564
Provider priority order (highest first):
@@ -566,6 +571,18 @@ Two providers are currently registered in `default_providers()`:
566
571
- **`LaravelModelProvider`** (`virtual_members/laravel.rs`): synthesizes virtual members for classes extending `Illuminate\Database\Eloquent\Model`. Produces relationship properties (methods returning `HasMany`, `HasOne`, `BelongsTo`, etc. generate a virtual property typed from the relationship's generic parameters), scope methods (both the `scopeActive` naming convention and the `#[Scope]` attribute from Laravel 11+ are supported; either style becomes `active()` as both static and instance), Builder-as-static forwarding (`User::where()->get()` resolves end-to-end), accessors (legacy `getXAttribute()` and modern `Attribute` casts), and cast properties (`$casts` array or `casts()` method entries are mapped to PHP types like `datetime` to `\Carbon\Carbon`, `boolean` to `bool`, custom cast classes to their `get()` return type). Highest priority among virtual member providers. Scope methods are also injected onto `Builder<Model>` instances via a post-generic-substitution hook in `type_hint_to_classes_depth` (see "Scope Methods on Builder Instances" below).
567
572
-**`PHPDocProvider`** (`virtual_members/phpdoc.rs`): parses `@method`, `@property`, `@property-read`, `@property-write`, and `@mixin` tags from the class-level docblock stored in `ClassInfo.class_docblock`. Explicit `@method` / `@property` tags are not parsed eagerly during AST extraction; instead, the raw docblock string is preserved and parsed lazily when `provide` is called. For `@mixin` tags, the provider loads the referenced classes and merges their public members. Within the provider, explicit tags take precedence over mixin members. Recurses into mixin-of-mixin chains up to `MAX_MIXIN_DEPTH`.
568
573
574
+
### Laravel Class Patches
575
+
576
+
After virtual members and interface members are merged, `resolve_class_fully_inner` calls `apply_laravel_patches(class, fqn)` from `virtual_members/laravel/patches.rs`. Unlike virtual member providers (which *add* new members), patches *modify* existing members' type information to fix framework-specific type inaccuracies that break chain resolution.
577
+
578
+
The patch system is centralized in a single module with one entry point. Dispatching is based on the fully-qualified class name and trait usage. Current patches:
579
+
580
+
1.**`Eloquent\Builder::__call` / `__callStatic` return type.** Laravel's `Builder::__call()` is declared as returning `mixed`, but in practice always returns `$this` (scope dispatch, macro dispatch, Query\Builder forwarding). The patch overrides the return type to `static` so that method chains through unknown calls preserve the Builder type.
581
+
582
+
2.**`Conditionable::when()` / `unless()` return type.** The trait declares `@return $this|TWhenReturnType` but the unresolved method-level template parameter `TWhenReturnType` prevents `is_self_like_type` from recognizing the return as self-referential, breaking method chain resolution on Builder and Collection. The patch replaces the return type with `$this`. Applied to the `Conditionable` trait itself, to `Eloquent\Builder` (which uses the trait), and to any class whose `used_traits` includes `Conditionable`.
583
+
584
+
3.**Bare `Builder` return types on scope methods.** Handled separately in `scopes.rs` (`is_bare_builder_type`) because it runs at scope-injection time (post-generic-substitution), not during `resolve_class_fully_inner`. Documented in the patch module as part of the inventory but not dispatched from it.
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+1Lines changed: 1 addition & 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
+
-**`Conditionable::when()` / `unless()` chain resolution.** Chained calls after `when()` or `unless()` on Eloquent Builder, Collection, and any class using the `Conditionable` trait now resolve correctly. Previously the unresolved `TWhenReturnType` template parameter in the return type broke chain continuation, so `Builder::where(...)->when(...)->get()` lost type information after `when()`.
12
13
-**`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.
13
14
-**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.
14
15
-**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.
0 commit comments