1macro_rules! static_regex {
2 ($pattern:expr) => {{
3 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4 RE.get_or_init(|| {
5 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6 })
7 }};
8}
9
10fn class_re() -> &'static regex::Regex {
11 static_regex!(
12 r"(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s\\]+))?"
13 )
14}
15fn method_re() -> &'static regex::Regex {
16 static_regex!(
17 r"(?:public|protected|private)\s+(?:static\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\S+))?"
18 )
19}
20fn use_re() -> &'static regex::Regex {
21 static_regex!(r"^use\s+([\w\\]+)(?:\s+as\s+(\w+))?\s*;")
22}
23fn extends_re() -> &'static regex::Regex {
24 static_regex!(r"extends\s+([\w\\]+)")
25}
26fn relation_re() -> &'static regex::Regex {
27 static_regex!(
28 r"\$this->(hasMany|hasOne|belongsTo|belongsToMany|morphMany|morphTo|morphOne|hasManyThrough|hasOneThrough)\s*\(\s*(\w+)::class"
29 )
30}
31fn fillable_re() -> &'static regex::Regex {
32 static_regex!(r"\$fillable\s*=\s*\[([\s\S]*?)\]")
33}
34fn casts_re() -> &'static regex::Regex {
35 static_regex!(r"\$casts\s*=\s*\[([\s\S]*?)\]")
36}
37fn scope_re() -> &'static regex::Regex {
38 static_regex!(r"public\s+function\s+(scope\w+)\s*\(")
39}
40fn migration_col_re() -> &'static regex::Regex {
41 static_regex!(r"\$table->(\w+)\s*\(\s*'(\w+)'(?:\s*,\s*(\d+))?\s*\)")
42}
43fn migration_table_re() -> &'static regex::Regex {
44 static_regex!(r"Schema::create\s*\(\s*'(\w+)'")
45}
46fn blade_directive_re() -> &'static regex::Regex {
47 static_regex!(
48 r"@(extends|section|yield|component|include|foreach|if|auth|guest|can|slot|push|stack|livewire|props)\s*\(([^)]*)\)"
49 )
50}
51
52pub fn compress_php_map(content: &str, filename: &str) -> Option<String> {
53 if filename.ends_with(".blade.php") {
54 return Some(compress_blade(content));
55 }
56
57 let parent = detect_laravel_type(content);
58 match parent.as_deref() {
59 Some("Model") => Some(compress_eloquent(content)),
60 Some("Controller") => Some(compress_controller(content)),
61 Some("Migration") if content.contains("Schema::") => Some(compress_migration(content)),
62 Some(
63 kind @ ("Job" | "Event" | "Listener" | "Notification" | "Mail" | "Policy" | "Request"),
64 ) => Some(compress_service_class(content, kind)),
65 _ => None,
66 }
67}
68
69fn detect_laravel_type(content: &str) -> Option<String> {
70 if let Some(caps) = extends_re().captures(content) {
71 let parent = caps[1].rsplit('\\').next().unwrap_or(&caps[1]);
72 return match parent {
73 "Model" | "Authenticatable" | "Pivot" => Some("Model".to_string()),
74 "Controller" => Some("Controller".to_string()),
75 "Migration" => Some("Migration".to_string()),
76 "Job" | "ShouldQueue" => Some("Job".to_string()),
77 "Event" => Some("Event".to_string()),
78 "Listener" | "ShouldHandleEventsAfterCommit" => Some("Listener".to_string()),
79 "Notification" => Some("Notification".to_string()),
80 "Mailable" | "Mail" => Some("Mail".to_string()),
81 "Policy" => Some("Policy".to_string()),
82 "FormRequest" => Some("Request".to_string()),
83 _ => {
84 if content.contains("Schema::create") || content.contains("Schema::table") {
85 Some("Migration".to_string())
86 } else {
87 Some(parent.to_string())
88 }
89 }
90 };
91 }
92 if content.contains("Schema::create") || content.contains("Schema::table") {
93 return Some("Migration".to_string());
94 }
95 None
96}
97
98fn compress_eloquent(content: &str) -> String {
99 let mut parts = Vec::new();
100
101 if let Some(caps) = class_re().captures(content) {
102 parts.push(format!("§ {} extends Model", &caps[1]));
103 }
104
105 let imports: Vec<String> = use_re()
106 .captures_iter(content)
107 .map(|c| c[1].rsplit('\\').next().unwrap_or(&c[1]).to_string())
108 .collect();
109 if !imports.is_empty() {
110 parts.push(format!(" deps: {}", imports.join(", ")));
111 }
112
113 if let Some(caps) = fillable_re().captures(content) {
114 let fields = extract_quoted_strings(&caps[1]);
115 if !fields.is_empty() {
116 parts.push(format!(" fillable: {}", fields.join(", ")));
117 }
118 }
119
120 if let Some(caps) = casts_re().captures(content) {
121 let casts = extract_cast_pairs(&caps[1]);
122 if !casts.is_empty() {
123 parts.push(format!(" casts: {}", casts.join(", ")));
124 }
125 }
126
127 let relations: Vec<String> = relation_re()
128 .captures_iter(content)
129 .map(|c| format!("{}({})", &c[1], &c[2]))
130 .collect();
131 if !relations.is_empty() {
132 parts.push(format!(" relations: {}", relations.join(", ")));
133 }
134
135 let scopes: Vec<String> = scope_re()
136 .captures_iter(content)
137 .map(|c| c[1].strip_prefix("scope").unwrap_or(&c[1]).to_string())
138 .collect();
139 if !scopes.is_empty() {
140 parts.push(format!(" scopes: {}", scopes.join(", ")));
141 }
142
143 let methods: Vec<String> = method_re()
144 .captures_iter(content)
145 .filter(|c| !c[1].starts_with("scope"))
146 .map(|c| {
147 let ret = c.get(3).map_or("", |m| m.as_str());
148 if ret.is_empty() {
149 c[1].to_string()
150 } else {
151 format!("{}→{}", &c[1], ret)
152 }
153 })
154 .collect();
155 if !methods.is_empty() {
156 parts.push(format!(" methods: {}", methods.join(", ")));
157 }
158
159 parts.join("\n")
160}
161
162fn compress_controller(content: &str) -> String {
163 let mut parts = Vec::new();
164
165 if let Some(caps) = class_re().captures(content) {
166 parts.push(format!("§ {}", &caps[1]));
167 }
168
169 let methods: Vec<String> = method_re()
170 .captures_iter(content)
171 .map(|c| {
172 let name = &c[1];
173 let params = compact_params(&c[2]);
174 let ret = c.get(3).map_or("", |m| m.as_str());
175 if ret.is_empty() {
176 format!(" λ {name}({params})")
177 } else {
178 format!(" λ {name}({params})→{ret}")
179 }
180 })
181 .collect();
182 parts.extend(methods);
183
184 parts.join("\n")
185}
186
187fn compress_migration(content: &str) -> String {
188 let mut parts = Vec::new();
189
190 let tables: Vec<String> = migration_table_re()
191 .captures_iter(content)
192 .map(|c| c[1].to_string())
193 .collect();
194
195 for table in &tables {
196 parts.push(format!("+{table} table:"));
197 }
198
199 let columns: Vec<String> = migration_col_re()
200 .captures_iter(content)
201 .filter_map(|c| {
202 let col_type = &c[1];
203 let col_name = &c[2];
204 if col_type == "table" || col_type == "create" {
205 return None;
206 }
207 let short_type = shorten_column_type(col_type);
208 Some(format!(" {col_name}:{short_type}"))
209 })
210 .collect();
211 parts.extend(columns);
212
213 let has_timestamps = content.contains("$table->timestamps()");
214 let has_soft_deletes = content.contains("softDeletes");
215 if has_timestamps {
216 parts.push(" timestamps".to_string());
217 }
218 if has_soft_deletes {
219 parts.push(" softDeletes".to_string());
220 }
221
222 if parts.is_empty() {
223 return "migration (empty)".to_string();
224 }
225 parts.join("\n")
226}
227
228fn compress_service_class(content: &str, kind: &str) -> String {
229 let mut parts = Vec::new();
230
231 if let Some(caps) = class_re().captures(content) {
232 parts.push(format!("§ {} [{}]", &caps[1], kind));
233 }
234
235 let constructor: Vec<String> = content
236 .lines()
237 .filter(|l| {
238 let t = l.trim();
239 t.contains("public function __construct") || (t.contains("private ") && t.contains('$'))
240 })
241 .take(1)
242 .flat_map(|l| {
243 if let Some(caps) = method_re().captures(l) {
244 vec![format!(" __construct({})", compact_params(&caps[2]))]
245 } else {
246 vec![]
247 }
248 })
249 .collect();
250 parts.extend(constructor);
251
252 let methods: Vec<String> = method_re()
253 .captures_iter(content)
254 .filter(|c| &c[1] != "__construct")
255 .map(|c| {
256 let ret = c.get(3).map_or("", |m| m.as_str());
257 if ret.is_empty() {
258 format!(" λ {}", &c[1])
259 } else {
260 format!(" λ {}→{}", &c[1], ret)
261 }
262 })
263 .collect();
264 parts.extend(methods);
265
266 parts.join("\n")
267}
268
269fn compress_blade(content: &str) -> String {
270 let mut parts = Vec::new();
271
272 let directives: Vec<String> = blade_directive_re()
273 .captures_iter(content)
274 .map(|c| {
275 let dir = &c[1];
276 let arg = c[2].trim().trim_matches('\'').trim_matches('"');
277 format!("@{dir}({arg})")
278 })
279 .collect();
280
281 if directives.is_empty() {
282 let line_count = content.lines().count();
283 return format!("blade template ({line_count}L, no directives)");
284 }
285
286 let mut seen = std::collections::HashSet::new();
287 for d in &directives {
288 if seen.insert(d.clone()) {
289 parts.push(d.clone());
290 }
291 }
292
293 parts.join("\n")
294}
295
296fn compact_params(params: &str) -> String {
297 params
298 .split(',')
299 .map(|p| {
300 let p = p.trim();
301 if let Some(var) = p.rsplit_once(' ') {
302 var.1.to_string()
303 } else {
304 p.to_string()
305 }
306 })
307 .collect::<Vec<_>>()
308 .join(", ")
309}
310
311fn extract_quoted_strings(text: &str) -> Vec<String> {
312 static_regex!(r"'(\w+)'")
313 .captures_iter(text)
314 .map(|c| c[1].to_string())
315 .collect()
316}
317
318fn extract_cast_pairs(text: &str) -> Vec<String> {
319 static_regex!(r"'(\w+)'\s*=>\s*'(\w+)'")
320 .captures_iter(text)
321 .map(|c| format!("{}:{}", &c[1], &c[2]))
322 .collect()
323}
324
325fn shorten_column_type(t: &str) -> &str {
326 match t {
327 "string" => "str",
328 "integer" => "int",
329 "bigInteger" | "unsignedBigInteger" => "bigint",
330 "boolean" => "bool",
331 "timestamp" | "timestampTz" => "ts",
332 "nullableTimestamps" => "ts?",
333 "text" => "text",
334 "json" | "jsonb" => "json",
335 "decimal" | "float" | "double" => "num",
336 "foreignId" | "foreignIdFor" => "fk",
337 "uuid" => "uuid",
338 "enum" => "enum",
339 "date" => "date",
340 "dateTime" | "dateTimeTz" => "datetime",
341 "id" => "id",
342 _ => t,
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn eloquent_model_compression() {
352 let model = r"<?php
353namespace App\Models;
354
355use Illuminate\Database\Eloquent\Model;
356use Illuminate\Database\Eloquent\Relations\HasMany;
357
358class User extends Model
359{
360 protected $fillable = ['name', 'email', 'password'];
361 protected $casts = ['verified_at' => 'datetime', 'is_admin' => 'boolean'];
362
363 public function posts(): HasMany
364 {
365 return $this->hasMany(Post::class);
366 }
367
368 public function scopeActive($query)
369 {
370 return $query->where('active', true);
371 }
372
373 public function getFullNameAttribute(): string
374 {
375 return $this->first_name . ' ' . $this->last_name;
376 }
377}
378";
379 let result = compress_eloquent(model);
380 assert!(result.contains("User extends Model"), "class header");
381 assert!(
382 result.contains("fillable: name, email, password"),
383 "fillable"
384 );
385 assert!(result.contains("verified_at:datetime"), "casts");
386 assert!(result.contains("hasMany(Post)"), "relations");
387 assert!(result.contains("scopes: Active"), "scopes");
388 }
389
390 #[test]
391 fn controller_compression() {
392 let ctrl = r"<?php
393namespace App\Http\Controllers;
394
395class UserController extends Controller
396{
397 public function index(): View
398 {
399 return view('users.index');
400 }
401
402 public function store(StoreUserRequest $request): RedirectResponse
403 {
404 User::create($request->validated());
405 return redirect()->route('users.index');
406 }
407
408 public function show(User $user): View
409 {
410 return view('users.show', compact('user'));
411 }
412}
413";
414 let result = compress_controller(ctrl);
415 assert!(result.contains("UserController"), "class name");
416 assert!(result.contains("λ index"), "index method");
417 assert!(result.contains("λ store"), "store method");
418 assert!(result.contains("λ show"), "show method");
419 }
420
421 #[test]
422 fn migration_compression() {
423 let migration = r"<?php
424use Illuminate\Database\Migrations\Migration;
425use Illuminate\Database\Schema\Blueprint;
426
427return new class extends Migration
428{
429 public function up(): void
430 {
431 Schema::create('users', function (Blueprint $table) {
432 $table->id('id');
433 $table->string('name');
434 $table->string('email', 255);
435 $table->timestamp('verified_at');
436 $table->boolean('is_admin');
437 $table->timestamps();
438 });
439 }
440};
441";
442 let result = compress_migration(migration);
443 assert!(result.contains("+users table:"), "table name");
444 assert!(result.contains("name:str"), "string column");
445 assert!(result.contains("email:str"), "string with length");
446 assert!(result.contains("is_admin:bool"), "boolean");
447 assert!(result.contains("timestamps"), "timestamps");
448 }
449
450 #[test]
451 fn blade_template_compression() {
452 let blade = r#"
453@extends('layouts.app')
454
455@section('content')
456<div class="container mx-auto px-4 py-8">
457 <h1 class="text-2xl font-bold mb-4">Users</h1>
458 @foreach($users as $user)
459 <div class="card">
460 @include('partials.user-card')
461 </div>
462 @endforeach
463
464 @if(auth()->check())
465 @component('components.admin-panel')
466 Admin content here
467 @endcomponent
468 @endif
469</div>
470@endsection
471"#;
472 let result = compress_blade(blade);
473 assert!(result.contains("@extends(layouts.app)"), "extends");
474 assert!(result.contains("@section(content)"), "section");
475 assert!(result.contains("@foreach"), "foreach");
476 assert!(result.contains("@include"), "include");
477 assert!(!result.contains("<div"), "no raw HTML");
478 }
479
480 #[test]
481 fn service_class_compression() {
482 let job = r"<?php
483namespace App\Jobs;
484
485class SendWelcomeEmail extends Job implements ShouldQueue
486{
487 public function __construct(
488 private User $user,
489 private string $template
490 ) {}
491
492 public function handle(Mailer $mailer): void
493 {
494 $mailer->send($this->template, $this->user);
495 }
496
497 public function failed(\Throwable $e): void
498 {
499 Log::error($e->getMessage());
500 }
501}
502";
503 let result = compress_service_class(job, "Job");
504 assert!(result.contains("SendWelcomeEmail [Job]"), "class + kind");
505 assert!(result.contains("λ handle"), "handle method");
506 assert!(result.contains("λ failed"), "failed method");
507 }
508
509 #[test]
510 fn detect_laravel_types() {
511 assert_eq!(
512 detect_laravel_type("class User extends Model {"),
513 Some("Model".to_string())
514 );
515 assert_eq!(
516 detect_laravel_type("class UserController extends Controller {"),
517 Some("Controller".to_string())
518 );
519 assert_eq!(
520 detect_laravel_type("Schema::create('users', function"),
521 Some("Migration".to_string())
522 );
523 }
524}