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