mod accessors;
mod builder;
mod casts;
mod factory;
mod helpers;
pub(crate) mod patches;
mod relationships;
mod scopes;
mod where_property;
pub use helpers::extends_eloquent_model;
pub(crate) use helpers::{accessor_method_candidates, camel_to_snake};
pub(crate) use accessors::is_accessor_method;
use accessors::{
extract_modern_accessor_type, is_legacy_accessor, is_modern_accessor,
legacy_accessor_property_name,
};
pub(crate) use relationships::count_property_to_relationship_method;
pub use relationships::infer_relationship_from_body;
pub(crate) use relationships::{RELATION_QUERY_METHODS, resolve_relation_chain};
use relationships::{
RelationshipKind, build_property_type, classify_relationship_typed, count_property_name,
extract_related_type_typed,
};
pub use scopes::build_scope_methods_for_builder;
use scopes::{build_scope_methods, is_scope_method};
use where_property::{build_where_property_methods_for_class, lowercase_method_names};
use std::collections::HashMap;
use std::sync::Arc;
use builder::build_builder_forwarded_methods;
use casts::cast_type_to_php_type;
pub use factory::LaravelFactoryProvider;
pub(crate) use factory::{factory_to_model_fqn, model_to_factory_fqn};
use crate::php_type::PhpType;
use crate::types::{ClassInfo, PropertyInfo};
use super::{ResolvedClassCache, VirtualMemberProvider, VirtualMembers};
pub(crate) const ELOQUENT_MODEL_FQN: &str = "Illuminate\\Database\\Eloquent\\Model";
pub const ELOQUENT_BUILDER_FQN: &str = "Illuminate\\Database\\Eloquent\\Builder";
pub(super) fn self_ref_subs(ty: PhpType) -> HashMap<String, PhpType> {
HashMap::from([
("static".to_owned(), ty.clone()),
("$this".to_owned(), ty.clone()),
("self".to_owned(), ty),
])
}
pub(crate) fn try_swap_custom_collection(
cls: ClassInfo,
base_fqn: &str,
generic_args: &[PhpType],
all_classes: &[Arc<ClassInfo>],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> ClassInfo {
if base_fqn != crate::types::ELOQUENT_COLLECTION_FQN || generic_args.is_empty() {
return cls;
}
let model_name = match generic_args.last().unwrap().base_name() {
Some(name) => name.to_string(),
None => return cls,
};
let model_class = find_class_in(all_classes, &model_name)
.cloned()
.or_else(|| class_loader(&model_name).map(Arc::unwrap_or_clone));
if let Some(ref mc) = model_class
&& let Some(coll_type) = mc.laravel().and_then(|l| l.custom_collection.as_ref())
{
let coll_name = coll_type.to_string();
find_class_in(all_classes, &coll_name)
.cloned()
.or_else(|| class_loader(&coll_name).map(Arc::unwrap_or_clone))
.unwrap_or(cls)
} else {
cls
}
}
pub(crate) fn try_inject_builder_scopes(
result: &mut ClassInfo,
raw_cls: &ClassInfo,
base_fqn: &str,
generic_args: &[PhpType],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) {
if !is_eloquent_builder_fqn(base_fqn, raw_cls) || generic_args.is_empty() {
return;
}
let model_name = match generic_args.first().unwrap().base_name() {
Some(name) => name,
None => return,
};
inject_scopes_and_model_methods(result, model_name, class_loader);
}
pub(crate) fn try_inject_mixin_builder_scopes(
result: &mut ClassInfo,
raw_cls: &ClassInfo,
generic_args: &[PhpType],
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) {
use std::collections::HashMap;
use crate::types::MAX_INHERITANCE_DEPTH;
use crate::util::short_name;
if generic_args.is_empty() || raw_cls.template_params.is_empty() {
return;
}
let mut root_subs: HashMap<String, PhpType> = HashMap::new();
for (i, param_name) in raw_cls.template_params.iter().enumerate() {
if let Some(arg) = generic_args.get(i) {
root_subs.insert(param_name.clone(), arg.clone());
}
}
let mut current = crate::inheritance::ClassRef::Borrowed(raw_cls);
let mut active_subs = root_subs;
let mut depth = 0u32;
loop {
if let Some(model_name) =
find_builder_mixin_model(¤t, &active_subs, raw_cls, class_loader)
{
inject_scopes_and_model_methods(result, &model_name, class_loader);
return;
}
let parent_name = match current.parent_class.as_ref() {
Some(name) => name.clone(),
None => break,
};
depth += 1;
if depth > MAX_INHERITANCE_DEPTH {
break;
}
let parent = match class_loader(&parent_name) {
Some(p) => p,
None => break,
};
let parent_short = short_name(&parent.name);
let type_args = current
.extends_generics
.iter()
.find(|(name, _)| short_name(name) == parent_short)
.map(|(_, args)| args);
if let Some(args) = type_args {
let mut level_subs = HashMap::new();
for (i, param_name) in parent.template_params.iter().enumerate() {
if let Some(arg) = args.get(i) {
let resolved = arg.substitute(&active_subs);
level_subs.insert(param_name.clone(), resolved);
}
}
active_subs = level_subs;
}
current = crate::inheritance::ClassRef::Owned(parent);
}
}
fn find_builder_mixin_model(
class: &ClassInfo,
active_subs: &std::collections::HashMap<String, crate::php_type::PhpType>,
root_cls: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> Option<String> {
use crate::util::short_name;
for mixin_name in &class.mixins {
if short_name(mixin_name) != "Builder" && mixin_name != ELOQUENT_BUILDER_FQN {
continue;
}
if let Some(ref mixin_cls) = class_loader(mixin_name) {
let fqn = mixin_cls.fqn();
if fqn != ELOQUENT_BUILDER_FQN && mixin_cls.name != ELOQUENT_BUILDER_FQN {
continue;
}
}
let mixin_short = short_name(mixin_name);
let mixin_args = class
.mixin_generics
.iter()
.find(|(name, _)| name == mixin_name || short_name(name) == mixin_short)
.map(|(_, args)| args.as_slice());
if let Some(args) = mixin_args
&& let Some(first_arg) = args.first()
{
let resolved = first_arg.substitute(active_subs);
if let Some(name) = resolved.base_name()
&& !root_cls.template_params.iter().any(|p| p == name)
{
return Some(name.to_string());
}
}
}
None
}
fn inject_scopes_and_model_methods(
result: &mut ClassInfo,
model_arg: &str,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) {
let scope_methods = build_scope_methods_for_builder(model_arg, class_loader);
for method in scope_methods {
let already_exists = result
.methods
.iter()
.any(|m| m.name == method.name && m.is_static == method.is_static);
if !already_exists {
result.methods.push(method);
}
}
inject_model_virtual_methods(result, model_arg, class_loader);
if let Some(model_class) = class_loader(model_arg) {
let existing = lowercase_method_names(&result.methods);
let where_methods = build_where_property_methods_for_class(&model_class, &existing);
for method in where_methods {
if !result
.methods
.iter()
.any(|m| m.name.eq_ignore_ascii_case(&method.name))
{
result.methods.push(method);
}
}
}
}
fn inject_model_virtual_methods(
builder: &mut ClassInfo,
model_name: &str,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) {
use crate::php_type::PhpType;
let model_class = match class_loader(model_name) {
Some(c) => c,
None => return,
};
if !extends_eloquent_model(&model_class, class_loader) {
return;
}
let resolved_model = if let Some(cache) = crate::virtual_members::active_resolved_class_cache()
{
crate::virtual_members::resolve_class_fully_cached(&model_class, class_loader, cache)
} else {
crate::virtual_members::resolve_class_fully(&model_class, class_loader)
};
let model_type = PhpType::Named(model_name.to_owned());
let subs = self_ref_subs(model_type);
for method in &resolved_model.methods {
if !method.is_virtual {
continue;
}
if builder
.methods
.iter()
.any(|m| m.name.eq_ignore_ascii_case(&method.name))
{
continue;
}
let mut forwarded = method.clone();
forwarded.is_static = false;
if let Some(ref mut ret) = forwarded.return_type {
*ret = ret.substitute(&subs);
}
builder.methods.push(forwarded);
}
}
fn is_eloquent_builder_fqn(base_fqn: &str, cls: &ClassInfo) -> bool {
base_fqn == ELOQUENT_BUILDER_FQN
|| cls.name == ELOQUENT_BUILDER_FQN
|| cls.fqn() == ELOQUENT_BUILDER_FQN
}
fn find_class_in<'a>(all_classes: &'a [Arc<ClassInfo>], name: &str) -> Option<&'a ClassInfo> {
let short = name.rsplit('\\').next().unwrap_or(name);
if name.contains('\\') {
let expected_ns = name.rsplit_once('\\').map(|(ns, _)| ns);
all_classes
.iter()
.find(|c| c.name == short && c.file_namespace.as_deref() == expected_ns)
.map(|c| c.as_ref())
} else {
all_classes
.iter()
.find(|c| c.name == short)
.map(|c| c.as_ref())
}
}
pub struct LaravelModelProvider;
fn carbon_type() -> PhpType {
PhpType::Named("Carbon\\Carbon".to_owned())
}
impl VirtualMemberProvider for LaravelModelProvider {
fn applies_to(
&self,
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
) -> bool {
extends_eloquent_model(class, class_loader)
}
fn provide(
&self,
class: &ClassInfo,
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
cache: Option<&ResolvedClassCache>,
) -> VirtualMembers {
let mut properties = Vec::new();
let mut methods = Vec::new();
let mut seen_props: std::collections::HashSet<String> = std::collections::HashSet::new();
if let Some(laravel) = class.laravel() {
for (column, cast_type) in &laravel.casts_definitions {
let php_type = cast_type_to_php_type(cast_type, class_loader);
seen_props.insert(column.clone());
properties.push(PropertyInfo::virtual_property_typed(
column,
Some(&php_type),
));
}
for column in &laravel.dates_definitions {
if !seen_props.insert(column.clone()) {
continue;
}
properties.push(PropertyInfo::virtual_property_typed(
column,
Some(&carbon_type()),
));
}
for (column, php_type) in &laravel.attributes_definitions {
if !seen_props.insert(column.clone()) {
continue;
}
properties.push(PropertyInfo::virtual_property_typed(column, Some(php_type)));
}
for column in &laravel.column_names {
if !seen_props.insert(column.clone()) {
continue;
}
properties.push(PropertyInfo::virtual_property_typed(
column,
Some(&PhpType::mixed()),
));
}
let timestamps_enabled = laravel.timestamps.unwrap_or(true);
if timestamps_enabled {
let created_col = match &laravel.created_at_name {
Some(Some(name)) => Some(name.as_str()),
Some(None) => None, None => Some("created_at"), };
let updated_col = match &laravel.updated_at_name {
Some(Some(name)) => Some(name.as_str()),
Some(None) => None, None => Some("updated_at"), };
for col in [created_col, updated_col].into_iter().flatten() {
if seen_props.insert(col.to_string()) {
properties.push(PropertyInfo::virtual_property_typed(
col,
Some(&carbon_type()),
));
}
}
}
}
for method in &class.methods {
if is_scope_method(method) {
let [instance_method, static_method] = build_scope_methods(method);
methods.push(instance_method);
methods.push(static_method);
continue;
}
if is_legacy_accessor(method) {
let prop_name = legacy_accessor_property_name(&method.name);
properties.push(PropertyInfo {
deprecation_message: method.deprecation_message.clone(),
..PropertyInfo::virtual_property_typed(&prop_name, method.return_type.as_ref())
});
continue;
}
if is_modern_accessor(method) {
let prop_name = camel_to_snake(&method.name);
let accessor_type = extract_modern_accessor_type(method);
properties.push(PropertyInfo {
deprecation_message: method.deprecation_message.clone(),
..PropertyInfo::virtual_property_typed(&prop_name, Some(&accessor_type))
});
continue;
}
let return_type = match method.return_type.as_ref() {
Some(rt) => rt,
None => continue,
};
let kind = match classify_relationship_typed(return_type) {
Some(k) => k,
None => continue,
};
let related_type = extract_related_type_typed(return_type);
let custom_collection = if kind == RelationshipKind::Collection {
related_type
.and_then(|t| t.base_name().and_then(class_loader))
.and_then(|related_class| {
related_class
.laravel
.as_ref()
.and_then(|l| l.custom_collection.as_ref().map(|c| c.to_string()))
})
} else {
None
};
let type_hint = build_property_type(kind, related_type, custom_collection.as_deref());
if let Some(ref th) = type_hint {
properties.push(PropertyInfo::virtual_property_typed(&method.name, Some(th)));
}
}
for method in &class.methods {
let return_type = match method.return_type.as_ref() {
Some(rt) => rt,
None => continue,
};
if classify_relationship_typed(return_type).is_none() {
continue;
}
let count_name = count_property_name(&method.name);
if !seen_props.insert(count_name.clone()) {
continue;
}
properties.push(PropertyInfo::virtual_property_typed(
&count_name,
Some(&PhpType::int()),
));
}
let forwarded = build_builder_forwarded_methods(class, class_loader, cache);
methods.extend(forwarded);
let existing = lowercase_method_names(&methods);
let where_static = build_where_property_methods_for_class(class, &existing);
for mut m in where_static {
m.is_static = true;
methods.push(m);
}
VirtualMembers {
methods,
properties,
constants: Vec::new(),
}
}
}
#[cfg(test)]
mod tests;