2026-05-18 16:33:19 -05:00
//! 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 ;
2026-05-21 14:35:42 -05:00
use crate ::dynamic ::framework ::{ FrameworkAdapter , FrameworkBinding , RouteShape } ;
2026-05-18 16:33:19 -05:00
use crate ::evidence ::EntryKind ;
use crate ::summary ::FuncSummary ;
use crate ::symbol ::Lang ;
use tree_sitter ::Node ;
use super ::php_routes ::{
2026-05-22 00:55:00 -05:00
bind_php_path_params , collect_php_middleware , find_laravel_static_route , find_php_function ,
php_class_name , php_formal_names , source_imports_laravel ,
2026-05-18 16:33:19 -05:00
} ;
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 < FrameworkBinding > {
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 ) ) ;
2026-05-21 14:35:42 -05:00
let ( method , path ) = find_laravel_static_route ( ast , file_bytes , & summary . name , controller ) ? ;
2026-05-18 16:33:19 -05:00
let formals = php_formal_names ( func_node , file_bytes ) ;
let request_params = bind_php_path_params ( & formals , & path ) ;
2026-05-22 00:55:00 -05:00
let middleware = collect_php_middleware ( ast , file_bytes ) ;
2026-05-18 16:33:19 -05:00
Some ( FrameworkBinding {
adapter : ADAPTER_NAME . to_owned ( ) ,
kind : EntryKind ::HttpRoute ,
route : Some ( RouteShape { method , path } ) ,
request_params ,
response_writer : None ,
2026-05-22 00:55:00 -05:00
middleware ,
2026-05-18 16:33:19 -05:00
} )
}
}
#[ 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 " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::get('/users/{id}', 'UserController@show'); \n class 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_eq! ( binding . adapter , " php-laravel " ) ;
assert_eq! ( binding . kind , EntryKind ::HttpRoute ) ;
let route = binding . route . expect ( " route " ) ;
assert_eq! ( route . method , HttpMethod ::GET ) ;
assert_eq! ( route . path , " /users/{id} " ) ;
let id = binding
. request_params
. iter ( )
. find ( | p | p . name = = " id " )
. unwrap ( ) ;
assert! ( matches! ( id . source , ParamSource ::PathSegment ( _ ) ) ) ;
}
#[ test ]
fn fires_on_post_with_closure ( ) {
let src : & [ u8 ] = b " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::post('/save', function ($payload) { return $payload; }); \n function save($payload) { return $payload; } \n " ;
let tree = parse ( src ) ;
let binding = PhpLaravelAdapter
. detect ( & summary ( " save " ) , tree . root_node ( ) , src )
. expect ( " binding " ) ;
let route = binding . route . unwrap ( ) ;
assert_eq! ( route . method , HttpMethod ::POST ) ;
assert_eq! ( route . path , " /save " ) ;
}
#[ test ]
fn fires_on_array_callable ( ) {
let src : & [ u8 ] = b " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::put('/users/{id}', [UserController::class, 'update']); \n class UserController { \n public function update($id) { return $id; } \n } \n " ;
let tree = parse ( src ) ;
let binding = PhpLaravelAdapter
. detect ( & summary ( " update " ) , tree . root_node ( ) , src )
. expect ( " binding " ) ;
assert_eq! ( binding . route . unwrap ( ) . method , HttpMethod ::PUT ) ;
}
#[ test ]
fn fires_on_double_colon_callable ( ) {
let src : & [ u8 ] = b " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::delete('/users/{id}', 'UserController::destroy'); \n class UserController { \n public function destroy($id) { return $id; } \n } \n " ;
let tree = parse ( src ) ;
let binding = PhpLaravelAdapter
. detect ( & summary ( " destroy " ) , tree . root_node ( ) , src )
. expect ( " binding " ) ;
assert_eq! ( binding . route . unwrap ( ) . method , HttpMethod ::DELETE ) ;
}
2026-05-22 00:55:00 -05:00
#[ test ]
fn populates_middleware_from_chained_call ( ) {
let src : & [ u8 ] = b " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::get('/users/{id}', 'UserController@show')->middleware('auth'); \n class 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 " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::get('/users', 'UserController@index'); \n class UserController { \n public function __construct() { $this->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
) ;
}
2026-05-18 16:33:19 -05:00
#[ test ]
fn skips_when_laravel_not_imported ( ) {
let src : & [ u8 ] = b " <?php \n function f($x) { return $x; } \n " ;
let tree = parse ( src ) ;
2026-05-21 14:35:42 -05:00
assert! (
PhpLaravelAdapter
. detect ( & summary ( " f " ) , tree . root_node ( ) , src )
. is_none ( )
) ;
2026-05-18 16:33:19 -05:00
}
#[ test ]
fn skips_when_route_mapping_does_not_reference_function ( ) {
let src : & [ u8 ] = b " <?php \n use Illuminate \\ Support \\ Facades \\ Route; \n Route::get('/users', 'UserController@show'); \n function helper($x) { return $x; } \n " ;
let tree = parse ( src ) ;
2026-05-21 14:35:42 -05:00
assert! (
PhpLaravelAdapter
. detect ( & summary ( " helper " ) , tree . root_node ( ) , src )
. is_none ( )
) ;
2026-05-18 16:33:19 -05:00
}
}