Kako strukturirati Laravel projekat za dugoročni rast
Laravel projekti često počinju isto — standardna struktura, nekoliko kontrolera, par modela, i sve funkcioniše. Problem nastaje šest meseci kasnije, kada aplikacija naraste i svaka promena u jednom delu sistema neočekivano pokvari nešto drugo.
Struktura projekta nije nešto o čemu razmišljate prvog dana. Ali odluke koje donesete (ili ne donesete) na početku, definišu koliko će vam biti lako da dodate novu funkcionalnost godinu dana kasnije.
Podrazumevana struktura nije dovoljna
Laravel dolazi sa jasnom strukturom direktorijuma — app/Http/Controllers, app/Models, app/Services. Za manje projekte, ovo je sasvim dovoljno. Ali kada broj kontrolera pređe trideset, a modeli počnu da imaju po dvesta linija, postaje jasno da organizacija po tipu fajla ne skalira.
Problem nije u Laravel-u — problem je u tome što grupišemo kod po tome šta jeste (kontroler, model, servis), umesto po tome šta radi (upravljanje korisnicima, procesiranje narudžbina, generisanje izveštaja).
Organizacija po domenu
Alternativa je organizacija po poslovnom domenu. Umesto:
app/
Http/Controllers/
UserController.php
OrderController.php
InvoiceController.php
Models/
User.php
Order.php
Invoice.php
Services/
UserService.php
OrderService.php
InvoiceService.php
Kod možete organizovati ovako:
app/
Domain/
Users/
UserController.php
User.php
UserService.php
UserPolicy.php
Orders/
OrderController.php
Order.php
OrderService.php
CreateOrderAction.php
Invoices/
InvoiceController.php
Invoice.php
GenerateInvoiceAction.php
Sada, kada trebate da razumete kako funkcionišu narudžbine, sve je na jednom mestu. Ne skačete između pet različitih direktorijuma da biste pratili tok jedne operacije.
Izdvajanje poslovne logike iz kontrolera
Najčešća greška u Laravel projektima su kontroleri od trista linija. Kontroler treba da primi zahtev, pozove odgovarajuću logiku i vrati odgovor. Ništa više.
Umesto:
public function store(Request $request)
{
$validated = $request->validate([
'email' => 'required|email|unique:users',
'name' => 'required|string|max:255',
]);
$user = User::create($validated);
$user->assignRole('member');
Mail::to($user)->send(new WelcomeEmail($user));
Activity::log('user_created', $user);
return response()->json($user, 201);
}
Izdvojite logiku u dedicirani action:
public function store(CreateUserRequest $request, CreateUserAction $action)
{
$user = $action->execute($request->validated());
return response()->json($user, 201);
}
CreateUserAction klasa enkapsulira svu logiku kreiranja korisnika — validacija je u Form Request-u, a kontroler samo koordinira. Ovo čini kod testabilnim bez HTTP sloja i omogućava pozivanje iste logike iz Artisan komandi, queue job-ova ili drugih delova sistema.
Form Request za validaciju
Laravel-ov Form Request mehanizam je jedan od najkorisnijih, a najređe korišćenih alata. Umesto validacije u kontroleru, kreirajte dedicirane request klase:
class CreateUserRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', User::class);
}
public function rules(): array
{
return [
'email' => ['required', 'email', 'unique:users'],
'name' => ['required', 'string', 'max:255'],
'role' => ['sometimes', 'string', 'in:admin,member'],
];
}
}
Autorizacija i validacija su na jednom mestu, kontroler je čist, a pravila su lako čitljiva i testabilna.
Eloquent modeli — manje je više
Eloquent modeli imaju tendenciju da postanu “god objects” — klase koje znaju sve i rade sve. Scope-ovi, mutatori, relacije, pomoćne metode — sve završi u jednom fajlu.
Držite modele fokusiranim na ono što Eloquent radi najbolje — definiciju tabele, relacija i cast-ova:
class Order extends Model
{
protected $fillable = ['user_id', 'status', 'total'];
protected $casts = [
'total' => 'decimal:2',
'status' => OrderStatus::class,
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
}
Query scope-ove koje koristite na više mesta možete zadržati, ali poslovnu logiku poput calculateDiscount() ili sendConfirmation() premestite u action klase ili servise.
Konfiguracija i environment varijable
Česta greška je direktno čitanje env() funkcije van config fajlova. Ovo ne radi kada je konfiguracija keširana (php artisan config:cache), a trebalo bi da bude keširana u produkciji.
Uvek čitajte vrednosti kroz config():
// Loše
$apiKey = env('PAYMENT_API_KEY');
// Dobro
$apiKey = config('services.payment.key');
Za servise koji zahtevaju više konfiguracionih vrednosti, kreirajte dedicirane config fajlove umesto trpanja svega u services.php.
Migracije i šema baze
Migracije su hronologija vaše baze — ne menjajte postojeće migracije koje su već pokrenute u produkciji. Umesto toga, kreirajte nove migracije za izmene.
Koristite deskriptivna imena:
php artisan make:migration add_discount_column_to_orders_table
Za enum kolone, koristite PHP enum-e sa cast-ovima umesto string vrednosti direktno u bazi. Ovo centralizuje moguće vrednosti i daje vam type safety.
Testiranje kao osnova
Struktura projekta koja omogućava lako testiranje je struktura koja omogućava lak razvoj. Ako ne možete da testirate klasu bez da pokrenete celu aplikaciju, to je znak da su zavisnosti previše isprepletane.
Action klase su inherentno testabilne:
public function test_creating_user_assigns_default_role(): void
{
$action = new CreateUserAction();
$user = $action->execute([
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->assertTrue($user->hasRole('member'));
}
Nema mock-ovanja HTTP zahteva, nema simuliranja request lifecycle-a. Pozovete metodu, proverite rezultat.
Zaključak
Dobra struktura projekta nije nešto što implementirate odjednom. To je proces koji evoluira sa projektom. Počnite sa konvencijama koje Laravel nudi, a kada osetite da određeni deo sistema postaje previše složen — tada je pravi trenutak za refaktorisanje.
Ključ je u konzistentnosti. Izaberite pristup, dokumentujte ga, i primenite ga dosledno kroz ceo projekat. Tim koji zna gde da traži kod i kako da doda novu funkcionalnost je tim koji brže isporučuje.