Recursively merge members from the given traits into `merged`. Traits can themselves `use` other traits (composition), so this function recurses up to `MAX_TRAIT_DEPTH` levels. Members that already exist in `merged` (by name) are skipped — this naturally implements the PHP precedence rule where the current class's own members win over trait members, and earlier-listed traits win over later ones.
(
merged: &mut ClassInfo,
trait_names: &[Atom],
ctx: &TraitContext<'_>,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
depth: u32,
dedup: &mut MergeDedup,
self_clas
| 799 | /// `@template T`, the inherited methods and properties have their |
| 800 | /// template parameter types replaced with the concrete types. |
| 801 | fn merge_traits_into( |
| 802 | merged: &mut ClassInfo, |
| 803 | trait_names: &[Atom], |
| 804 | ctx: &TraitContext<'_>, |
| 805 | class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>, |
| 806 | depth: u32, |
| 807 | dedup: &mut MergeDedup, |
| 808 | self_class_name: &str, |
| 809 | ) { |
| 810 | if depth > MAX_TRAIT_DEPTH { |
| 811 | return; |
| 812 | } |
| 813 | |
| 814 | for trait_name in trait_names { |
| 815 | let trait_info = if let Some(t) = class_loader(trait_name) { |
| 816 | t |
| 817 | } else { |
| 818 | continue; |
| 819 | }; |
| 820 | |
| 821 | // Build a substitution map for this trait if the using class |
| 822 | // declared `@use TraitName<Type1, Type2>` and the trait has |
| 823 | // `@template` parameters. |
| 824 | let mut trait_subs = |
| 825 | build_trait_substitution_map(trait_name, &trait_info, ctx.use_generics); |
| 826 | |
| 827 | // ── Convention-based HasFactory fallback ───────────────── |
| 828 | // When a model uses `HasFactory` without `@use HasFactory<X>`, |
| 829 | // derive the factory class from the naming convention |
| 830 | // (e.g. `App\Models\User` → `Database\Factories\UserFactory`) |
| 831 | // and substitute `TFactory` automatically. |
| 832 | if trait_subs.is_empty() |
| 833 | && !trait_info.template_params.is_empty() |
| 834 | && is_has_factory_trait(trait_name) |
| 835 | && extends_eloquent_model(merged, class_loader) |
| 836 | { |
| 837 | let model_fqn = merged.fqn(); |
| 838 | let factory_fqn = model_to_factory_fqn(&model_fqn); |
| 839 | if class_loader(&factory_fqn).is_some() { |
| 840 | for param in &trait_info.template_params { |
| 841 | trait_subs.insert(param.to_string(), PhpType::Named(factory_fqn.clone())); |
| 842 | } |
| 843 | } |
| 844 | } |
| 845 | |
| 846 | // ── Template bound fallback ───────────────────────────── |
| 847 | // When a class uses a generic trait without `@use` generics |
| 848 | // and no convention-based provider filled the map, fall back |
| 849 | // to the template parameter bounds (e.g. `@template T of object` |
| 850 | // → `object`) so inherited methods don't leak raw template names. |
| 851 | if !trait_info.template_params.is_empty() { |
| 852 | for param_name in &trait_info.template_params { |
| 853 | if !trait_subs.contains_key(param_name.to_string().as_str()) { |
| 854 | let bound = trait_info |
| 855 | .template_param_bounds |
| 856 | .get(param_name) |
| 857 | .cloned() |
| 858 | .unwrap_or_else(PhpType::mixed); |
no test coverage detected