//! Laravel [`super::super::FrameworkAdapter`] (Phase 16 — Track L.14). //! //! Two recognition shapes: //! //! - Closure route: `Route::get('/path', function ($payload) {…})` //! declared at top level — the closure's function name is the //! enclosing summary's name (the static-analysis side already //! stamps anonymous closures with a synthetic name slot). //! - Controller-method route: //! `Route::get('/path', 'UserController@show')` / //! `Route::post('/path', [UserController::class, 'save'])` plus //! a `class UserController { public function show($id) {…} }` //! declaration in the same file. #[cfg(test)] use crate::dynamic::framework::HttpMethod; use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding}; use crate::evidence::EntryKind; use crate::summary::FuncSummary; use crate::symbol::Lang; use tree_sitter::Node; use super::php_routes::{ bind_php_path_params, collect_php_middleware, find_laravel_static_route_shape, find_php_function, php_class_name, php_formal_names, source_imports_laravel, }; pub struct PhpLaravelAdapter; const ADAPTER_NAME: &str = "php-laravel"; impl FrameworkAdapter for PhpLaravelAdapter { fn name(&self) -> &'static str { ADAPTER_NAME } fn lang(&self) -> Lang { Lang::Php } fn detect( &self, summary: &FuncSummary, ast: Node<'_>, file_bytes: &[u8], ) -> Option { if !source_imports_laravel(file_bytes) { return None; } let (func_node, class) = find_php_function(ast, file_bytes, &summary.name)?; let controller = class.and_then(|c| php_class_name(c, file_bytes)); let route = find_laravel_static_route_shape(ast, file_bytes, &summary.name, controller)?; let formals = php_formal_names(func_node, file_bytes); let request_params = bind_php_path_params(&formals, &route.path); let middleware = collect_php_middleware(ast, file_bytes); Some(FrameworkBinding { adapter: ADAPTER_NAME.to_owned(), kind: EntryKind::HttpRoute, route: Some(route), request_params, response_writer: None, middleware, }) } } #[cfg(test)] mod tests { use super::*; use crate::dynamic::framework::ParamSource; fn parse(src: &[u8]) -> tree_sitter::Tree { let mut parser = tree_sitter::Parser::new(); let lang = tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP); parser.set_language(&lang).unwrap(); parser.parse(src, None).unwrap() } fn summary(name: &str) -> FuncSummary { FuncSummary { name: name.into(), lang: "php".into(), ..Default::default() } } #[test] fn fires_on_route_get_with_controller_method() { let src: &[u8] = b"middleware('auth');\nclass UserController {\n public function show($id) { return $id; }\n}\n"; let tree = parse(src); let binding = PhpLaravelAdapter .detect(&summary("show"), tree.root_node(), src) .expect("binding"); assert!( binding.middleware.iter().any(|m| m.name == "auth"), "got {:?}", binding.middleware ); } #[test] fn populates_middleware_from_constructor_call() { let src: &[u8] = b"middleware('auth:sanctum'); }\n public function index() { return 1; }\n}\n"; let tree = parse(src); let binding = PhpLaravelAdapter .detect(&summary("index"), tree.root_node(), src) .expect("binding"); assert!( binding.middleware.iter().any(|m| m.name == "auth:sanctum"), "got {:?}", binding.middleware ); } #[test] fn skips_when_laravel_not_imported() { let src: &[u8] = b"